diff --git a/README.md b/README.md index 9249a17..dbbafe4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 95ace6f..56802bf 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -419,11 +419,12 @@ Status Indicators ~ Test cases use competitive programming terminology with color highlighting: - AC Accepted (passed) - Green - WA Wrong Answer (output mismatch) - Red - TLE Time Limit Exceeded (timeout) - Orange - MLE Memory Limit Exceeded Error (heuristic) - Orange - RTE Runtime Error (other non-zero exit code) - Purple + AC Accepted (passed) + WA Wrong Answer (output mismatch) + TLE Time Limit Exceeded (timeout) + MLE Memory Limit Exceeded Error (heuristic) + 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 CpTestWA Red foreground for WA status CpTestTLE Orange foreground for TLE status + CpTestMLE Orange foreground for MLE status CpTestRTE Purple foreground for RTE status - CpTestPending Gray foreground for pending tests + CpTestNA Gray foreground for remaining state ANSI Color Groups ~ diff --git a/lua/cp/health.lua b/lua/cp/health.lua index 7b5ba37..c5e5113 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -34,11 +34,18 @@ local function check_required() vim.health.info('Python virtual environment not set up (created on first scrape)') end - local cap = utils.time_capability() - if cap.ok then - vim.health.ok('GNU time found: ' .. cap.path) + local time_cap = utils.time_capability() + if time_cap.ok then + vim.health.ok('GNU time found: ' .. time_cap.path) 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 diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index 72f41f4..4ad0f3b 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -63,67 +63,62 @@ function M.compile(language_config, substitutions) return r end -local function parse_and_strip_time_v(output, memory_mb) - local lines = vim.split(output or '', '\n', { plain = true }) - - local timing_idx - for i = #lines, 1, -1 do - if lines[i]:match('^%s*Command being timed:') then - timing_idx = i +local function parse_and_strip_time_v(output) + local s = output or '' + local last_i, from = nil, 1 + while true do + local i = string.find(s, 'Command being timed:', from, true) + if not i then break end + last_i, from = i, i + 1 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 + if not last_i then + return s, 0 end - local start_idx = timing_idx - local k = timing_idx - 1 - while k >= 1 and lines[k]:match('^%s*Command ') do - start_idx = k + local k = last_i - 1 + while k >= 1 do + local ch = s:sub(k, k) + if ch ~= ' ' and ch ~= '\t' then + break + end k = k - 1 end - local peak_mb, mled = 0, false - for j = timing_idx, #lines do - local kb = lines[j]:match('Maximum resident set size %(kbytes%):%s*(%d+)') + local head = s:sub(1, k) + local tail = s:sub(last_i) + + 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 - peak_mb = tonumber(kb) / 1024.0 - if memory_mb and memory_mb > 0 and peak_mb > memory_mb then - mled = true - end + peak_kb = tonumber(kb) or 0 end end - for j = #lines, start_idx, -1 do - table.remove(lines, j) - end - - while #lines > 0 and lines[#lines]:match('^%s*$') do - table.remove(lines, #lines) - end - - return table.concat(lines, '\n'), peak_mb, mled + local peak_mb = peak_kb / 1024.0 + head = head:gsub('\n+$', '') + return head, peak_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() @@ -142,17 +137,12 @@ function M.run(cmd, stdin, timeout_ms, memory_mb) 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('out of memory', 1, true) + or lower:find('oom', 1, true) or lower:find('enomem', 1, true) + local near_cap = peak_mb >= (0.90 * memory_mb) - 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 + local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled) if tled then logger.log(('Execution timed out in %.1fms.'):format(dt)) diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 411769d..b7af68b 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -122,9 +122,8 @@ local function run_single_test_case(contest_config, cp_config, test_case) local cmd = build_command(language_config, substitutions) local stdin_content = (test_case.input or '') .. '\n' - local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) - or 2000 - local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or nil + local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) or 0 + local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or 0 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, tled = r.tled or false, mled = r.mled or false, - rss_mb = r.peak_mb, + rss_mb = r.peak_mb or 0, } end @@ -284,6 +283,7 @@ function M.handle_compilation_failure(output) tc.signal = '' tc.tled = false tc.mled = false + tc.rss_mb = 0 end end diff --git a/lua/cp/runner/run_render.lua b/lua/cp/runner/run_render.lua index 42de606..027aae3 100644 --- a/lua/cp/runner/run_render.lua +++ b/lua/cp/runner/run_render.lua @@ -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 = 'N/A', highlight_group = 'CpTestNA' } end local function format_exit_code(code) @@ -356,7 +354,7 @@ function M.get_highlight_groups() CpTestTLE = { fg = '#f59e0b' }, CpTestMLE = { fg = '#f59e0b' }, CpTestRTE = { fg = '#8b5cf6' }, - CpTestPending = { fg = '#6b7280' }, + CpTestNA = { fg = '#6b7280' }, CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' }, CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' }, } diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 033c924..c3ff310 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -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