feat(test_panel): integrate scraped data

This commit is contained in:
Barrett Ruth 2025-09-19 20:41:19 -04:00
parent fe25b00537
commit 793063a68e
11 changed files with 160 additions and 40 deletions

View file

@ -100,7 +100,6 @@ Here's an example configuration with lazy.nvim: >
extension = "py",
},
default_language = "cpp",
timeout_ms = 2000,
},
},
hooks = {
@ -156,8 +155,6 @@ Here's an example configuration with lazy.nvim: >
• {python} (`LanguageConfig`) Python language configuration.
• {default_language} (`string`, default: `"cpp"`) Default language when
`--lang` not specified.
• {timeout_ms} (`number`, default: `2000`) Execution timeout in
milliseconds.
*cp.LanguageConfig*

View file

@ -7,6 +7,8 @@
---@field expires_at? number
---@field test_cases? CachedTestCase[]
---@field test_cases_cached_at? number
---@field timeout_ms? number
---@field memory_mb? number
---@class Problem
---@field id string
@ -167,12 +169,16 @@ end
---@param contest_id string
---@param problem_id? string
---@param test_cases CachedTestCase[]
function M.set_test_cases(platform, contest_id, problem_id, test_cases)
---@param timeout_ms? number
---@param memory_mb? number
function M.set_test_cases(platform, contest_id, problem_id, test_cases, timeout_ms, memory_mb)
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
problem_id = { problem_id, { 'string', 'nil' }, true },
test_cases = { test_cases, 'table' },
timeout_ms = { timeout_ms, { 'number', 'nil' }, true },
memory_mb = { memory_mb, { 'number', 'nil' }, true },
})
local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
@ -185,7 +191,33 @@ function M.set_test_cases(platform, contest_id, problem_id, test_cases)
cache_data[platform][problem_key].test_cases = test_cases
cache_data[platform][problem_key].test_cases_cached_at = os.time()
if timeout_ms then
cache_data[platform][problem_key].timeout_ms = timeout_ms
end
if memory_mb then
cache_data[platform][problem_key].memory_mb = memory_mb
end
M.save()
end
---@param platform string
---@param contest_id string
---@param problem_id? string
---@return number?, number?
function M.get_constraints(platform, contest_id, problem_id)
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
problem_id = { problem_id, { 'string', 'nil' }, true },
})
local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
if not cache_data[platform] or not cache_data[platform][problem_key] then
return nil, nil
end
local problem_data = cache_data[platform][problem_key]
return problem_data.timeout_ms, problem_data.memory_mb
end
return M

View file

@ -18,13 +18,11 @@
---@field cpp LanguageConfig
---@field python LanguageConfig
---@field default_language string
---@field timeout_ms number
---@class PartialContestConfig
---@field cpp? PartialLanguageConfig
---@field python? PartialLanguageConfig
---@field default_language? string
---@field timeout_ms? number
---@class Hooks
---@field before_run? fun(ctx: ProblemContext)

View file

@ -103,7 +103,7 @@ end
---@param cmd string[]
---@param input_data string
---@param timeout_ms integer
---@param timeout_ms number
---@return ExecuteResult
local function execute_command(cmd, input_data, timeout_ms)
vim.validate({
@ -279,7 +279,7 @@ function M.run_problem(ctx, contest_config, is_debug)
end
local run_cmd = build_command(language_config.test, language_config.executable, substitutions)
local exec_result = execute_command(run_cmd, input_data, contest_config.timeout_ms)
local exec_result = execute_command(run_cmd, input_data, timeout_ms)
local formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
local output_buf = vim.fn.bufnr(ctx.output_file)

View file

@ -7,6 +7,8 @@
---@field problem_id string
---@field url? string
---@field tests? ScraperTestCase[]
---@field timeout_ms? number
---@field memory_mb? number
---@field error? string
local M = {}
@ -152,7 +154,7 @@ function M.scrape_contest_metadata(platform, contest_id)
end
---@param ctx ProblemContext
---@return {success: boolean, problem_id: string, test_count?: number, test_cases?: ScraperTestCase[], url?: string, error?: string}
---@return {success: boolean, problem_id: string, test_count?: number, test_cases?: ScraperTestCase[], timeout_ms?: number, memory_mb?: number, url?: string, error?: string}
function M.scrape_problem(ctx)
vim.validate({
ctx = { ctx, 'table' },
@ -277,6 +279,24 @@ function M.scrape_problem(ctx)
vim.fn.writefile(vim.split(input_content, '\n', true), input_file)
vim.fn.writefile(vim.split(expected_content, '\n', true), expected_file)
end
local cached_test_cases = {}
for i, test_case in ipairs(data.tests) do
table.insert(cached_test_cases, {
index = i,
input = test_case.input,
expected = test_case.expected,
})
end
cache.set_test_cases(
ctx.contest,
ctx.contest_id,
ctx.problem_id,
cached_test_cases,
data.timeout_ms,
data.memory_mb
)
end
return {
@ -284,6 +304,8 @@ function M.scrape_problem(ctx)
problem_id = ctx.problem_name,
test_count = data.tests and #data.tests or 0,
test_cases = data.tests,
timeout_ms = data.timeout_ms,
memory_mb = data.memory_mb,
url = data.url,
}
end

View file

@ -12,6 +12,10 @@
---@field signal string?
---@field timed_out boolean?
---@class ProblemConstraints
---@field timeout_ms number
---@field memory_mb number
---@class RunPanelState
---@field test_cases TestCase[]
---@field current_index number
@ -19,6 +23,7 @@
---@field namespace number?
---@field is_active boolean
---@field saved_layout table?
---@field constraints ProblemConstraints?
local M = {}
local constants = require('cp.constants')
@ -32,6 +37,7 @@ local run_panel_state = {
namespace = nil,
is_active = false,
saved_layout = nil,
constraints = nil,
}
---@param index number
@ -114,6 +120,25 @@ local function parse_test_cases_from_files(input_file, expected_file)
return test_cases
end
---@param platform string
---@param contest_id string
---@param problem_id string?
---@return ProblemConstraints?
local function load_constraints_from_cache(platform, contest_id, problem_id)
local cache = require('cp.cache')
cache.load()
local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id)
if timeout_ms and memory_mb then
return {
timeout_ms = timeout_ms,
memory_mb = memory_mb,
}
end
return nil
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@param test_case TestCase
@ -177,10 +202,15 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
local stdin_content = test_case.input .. '\n'
local start_time = vim.uv.hrtime()
local timeout_ms = run_panel_state.constraints and run_panel_state.constraints.timeout_ms or 2000
if not run_panel_state.constraints then
logger.log('no problem constraints available, using default 2000ms timeout')
end
local result = vim
.system(run_cmd, {
stdin = stdin_content,
timeout = contest_config.timeout_ms or 2000,
timeout = timeout_ms,
text = true,
})
:wait()
@ -241,8 +271,17 @@ function M.load_test_cases(ctx, state)
run_panel_state.test_cases = test_cases
run_panel_state.current_index = 1
run_panel_state.constraints =
load_constraints_from_cache(state.platform, state.contest_id, state.problem_id)
logger.log(('loaded %d test case(s)'):format(#test_cases))
local constraint_info = run_panel_state.constraints
and string.format(
' with %dms/%dMB limits',
run_panel_state.constraints.timeout_ms,
run_panel_state.constraints.memory_mb
)
or ''
logger.log(('loaded %d test case(s)%s'):format(#test_cases, constraint_info))
return #test_cases > 0
end

View file

@ -55,24 +55,34 @@ end
-- Compute column widths + aggregates
local function compute_cols(test_state)
local w = { num = 3, status = 8, time = 6, exit = 11 }
local w = { num = 3, status = 8, time = 6, exit = 11, limits = 12 }
local limits_str = ''
if test_state.constraints then
limits_str =
string.format('%d/%.0f', test_state.constraints.timeout_ms, test_state.constraints.memory_mb)
else
limits_str = ''
end
for i, tc in ipairs(test_state.test_cases) do
local prefix = (i == test_state.current_index) and '>' or ' '
w.num = math.max(w.num, #(prefix .. i))
w.status = math.max(w.status, #(' ' .. M.get_status_info(tc).text))
local time_str = tc.time_ms and (string.format('%.2f', tc.time_ms) .. 'ms') or ''
local time_str = tc.time_ms and string.format('%.2f', tc.time_ms) or ''
w.time = math.max(w.time, #time_str)
w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code)))
w.limits = math.max(w.limits, #limits_str)
end
w.num = math.max(w.num, #' #')
w.status = math.max(w.status, #' Status')
w.time = math.max(w.time, #' Time')
w.time = math.max(w.time, #' Runtime (ms)')
w.exit = math.max(w.exit, #' Exit Code')
w.limits = math.max(w.limits, #' Time (ms)/Mem (MB)')
local sum = w.num + w.status + w.time + w.exit
local inner = sum + 3 -- three inner vertical dividers
local sum = w.num + w.status + w.time + w.exit + w.limits
local inner = sum + 4 -- four inner vertical dividers
local total = inner + 2 -- two outer borders
return { w = w, sum = sum, inner = inner, total = total }
end
@ -86,6 +96,14 @@ local function center(text, width)
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
end
local function right_align(text, width)
local pad = width - #text
if pad <= 0 then
return text
end
return string.rep(' ', pad) .. text
end
local function top_border(c)
local w = c.w
return ''
@ -96,6 +114,8 @@ local function top_border(c)
.. string.rep('', w.time)
.. ''
.. string.rep('', w.exit)
.. ''
.. string.rep('', w.limits)
.. ''
end
@ -109,6 +129,8 @@ local function row_sep(c)
.. string.rep('', w.time)
.. ''
.. string.rep('', w.exit)
.. ''
.. string.rep('', w.limits)
.. ''
end
@ -122,6 +144,8 @@ local function bottom_border(c)
.. string.rep('', w.time)
.. ''
.. string.rep('', w.exit)
.. ''
.. string.rep('', w.limits)
.. ''
end
@ -135,6 +159,8 @@ local function flat_fence_above(c)
.. string.rep('', w.time)
.. ''
.. string.rep('', w.exit)
.. ''
.. string.rep('', w.limits)
.. ''
end
@ -148,6 +174,8 @@ local function flat_fence_below(c)
.. string.rep('', w.time)
.. ''
.. string.rep('', w.exit)
.. ''
.. string.rep('', w.limits)
.. ''
end
@ -162,34 +190,45 @@ local function header_line(c)
.. ''
.. center('Status', w.status)
.. ''
.. center('Time', w.time)
.. center('Runtime (ms)', w.time)
.. ''
.. center('Exit Code', w.exit)
.. ''
.. center('Time (ms)/Mem (MB)', w.limits)
.. ''
end
local function data_row(c, idx, tc, is_current)
local function data_row(c, idx, tc, is_current, test_state)
local w = c.w
local prefix = is_current and '>' or ' '
local status = M.get_status_info(tc)
local time = tc.time_ms and (string.format('%.2f', tc.time_ms) .. 'ms') or ''
local time = tc.time_ms and string.format('%.2f', tc.time_ms) or ''
local exit = format_exit_code(tc.code)
local limits = ''
if test_state.constraints then
limits =
string.format('%d/%.0f', test_state.constraints.timeout_ms, test_state.constraints.memory_mb)
else
limits = ''
end
local line = ''
.. center(prefix .. idx, w.num)
.. ''
.. center(status.text, w.status)
.. right_align(status.text, w.status)
.. ''
.. center(time, w.time)
.. right_align(time, w.time)
.. ''
.. center(exit, w.exit)
.. right_align(exit, w.exit)
.. ''
.. right_align(limits, w.limits)
.. ''
local hi
if status.text ~= '' then
local pad = w.status - #status.text
local left = math.floor(pad / 2)
local status_start_col = 1 + w.num + 1 + left
local status_start_col = 1 + w.num + 1 + pad
local status_end_col = status_start_col + #status.text
hi = {
col_start = status_start_col,
@ -213,7 +252,7 @@ function M.render_test_list(test_state)
for i, tc in ipairs(test_state.test_cases) do
local is_current = (i == test_state.current_index)
local row, hi = data_row(c, i, tc, is_current)
local row, hi = data_row(c, i, tc, is_current, test_state)
table.insert(lines, row)
if hi then
hi.line = #lines - 1

View file

@ -11,7 +11,7 @@ from bs4 import BeautifulSoup, Tag
from .models import MetadataResult, ProblemSummary, TestCase, TestsResult
def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, int]:
def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, float]:
timeout_ms = None
memory_mb = None
@ -26,7 +26,8 @@ def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, int]:
memory_match = re.search(r"Memory Limit:\s*(\d+)\s*MiB", text)
if memory_match:
memory_mb = int(memory_match.group(1))
memory_mib = int(memory_match.group(1))
memory_mb = round(memory_mib * 1.048576, 2)
break
if timeout_ms is None:

View file

@ -140,7 +140,7 @@ def parse_problem_url(contest_id: str, problem_letter: str) -> str:
)
def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, int]:
def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, float]:
import re
timeout_ms = None
@ -162,7 +162,7 @@ def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, int]:
text = memory_limit_div.get_text().strip()
match = re.search(r"(\d+) megabytes", text)
if match:
memory_mb = int(match.group(1))
memory_mb = float(match.group(1))
if memory_mb is None:
raise ValueError("Could not find valid memory limit in memory-limit section")

View file

@ -19,7 +19,7 @@ def parse_problem_url(problem_input: str) -> str | None:
return None
def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, int]:
def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, float]:
timeout_ms = None
memory_mb = None
@ -39,7 +39,7 @@ def extract_problem_limits(soup: BeautifulSoup) -> tuple[int, int]:
if "Memory limit:" in text:
match = re.search(r"Memory limit:\s*(\d+)\s*MB", text)
if match:
memory_mb = int(match.group(1))
memory_mb = float(match.group(1))
if timeout_ms is None:
raise ValueError("Could not find valid timeout in task-constraints section")

View file

@ -13,14 +13,6 @@ class ProblemSummary:
name: str
@dataclass
class Problem:
id: str
name: str
timeout_ms: int
memory_mb: int
@dataclass
class ScrapingResult:
success: bool
@ -40,4 +32,4 @@ class TestsResult(ScrapingResult):
url: str
tests: list[TestCase]
timeout_ms: int
memory_mb: int
memory_mb: float