Merge pull request #37 from barrett-ruth/feat/test-mode

Test Mode
This commit is contained in:
Barrett Ruth 2025-09-16 05:03:04 +02:00 committed by GitHub
commit f34b9554f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 695 additions and 70 deletions

View file

@ -0,0 +1,7 @@
vim.opt_local.number = false
vim.opt_local.relativenumber = false
vim.opt_local.statuscolumn = ""
vim.opt_local.signcolumn = "no"
vim.opt_local.foldcolumn = "0"
vim.opt_local.wrap = true
vim.opt_local.linebreak = true

View file

@ -55,6 +55,10 @@ Action Commands ~
:CP debug Compile with debug flags and run current problem.
Includes sanitizers and debug symbols.
:CP test Toggle test panel for individual test case
debugging. Shows per-test results with
vim-native navigation and execution controls.
Navigation Commands ~
:CP next Navigate to next problem in current contest.
@ -130,7 +134,7 @@ Optional configuration with lazy.nvim: >
• {filename}? (`function`) Custom filename generation function.
`function(contest, contest_id, problem_id, config, language)`
Should return full filename with extension.
(default: uses problem_id or contest_id)
(default: concats contest_id and problem id)
*cp.ContestConfig*
@ -235,10 +239,15 @@ Example: Setting up and solving AtCoder contest ABC324
4. Code your solution, then test: >
:CP run
<
5. If needed, debug: >
5. If test fails, debug individual test cases: >
:CP test
< Navigate with j/k, run specific tests with <enter>
Exit test panel with q or :CP test when done
6. If needed, compile with debug flags: >
:CP debug
<
6. Move to next problem: >
7. Move to next problem: >
:CP next
< This automatically sets up problem B
@ -250,6 +259,82 @@ Example: Quick setup for single Codeforces problem >
:CP run " Test immediately
<
TEST PANEL *cp-test*
The test panel provides individual test case debugging for competitive
programming problems, particularly useful for Codeforces where multiple
test cases are combined into single input/output files.
Activation ~
*:CP-test*
:CP test Toggle test panel on/off. When activated,
replaces current layout with test interface.
Toggle again to restore original layout.
Interface ~
The test panel displays a list of test cases with their status and details
for the currently selected test case: >
┌─ Test Cases ───────────────────────────────────────────────┐
│ 1 ✓ PASS 12ms │
│ 2 ✗ FAIL 45ms │
│> 3 ✓ PASS 8ms <-- current selection │
│ 4 ? PENDING │
│ │
│ ── Test 3 ── │
│ Input: │ Expected: │ Actual: │
│ 5 3 │ 8 │ 8 │
│ │ │ │
│ │
│ j/k: navigate <space>: toggle <enter>: run a: run all │
└────────────────────────────────────────────────────────────┘
<
Test Status Indicators ~
✓ PASS Test case passed (green)
✗ FAIL Test case failed (red)
? PENDING Test case not yet executed (yellow)
⟳ RUNNING Test case currently executing (blue)
Keymaps ~
*cp-test-keys*
j / <Down> Navigate to next test case
k / <Up> Navigate to previous test case
<space> Toggle selection of current test case
<enter> Run selected test cases
a Run all test cases
r Re-run failed test cases only
c Clear all test results
q / <esc> Exit test panel (restore layout)
Test Case Sources ~
Test cases are loaded in priority order:
1. Individual scraped test cases (preferred for Codeforces)
2. Combined input/output files from io/ directory (fallback)
For Codeforces problems, the plugin attempts to parse individual test
cases from the scraped contest data, enabling precise debugging of
specific test case failures.
For AtCoder and CSES problems, which typically provide single test
cases, the combined input/output approach is used.
Execution Details ~
Each test case shows:
• Input data provided to your solution
• Expected output from the problem statement
• Actual output produced by your solution
• Execution time in milliseconds
• Error messages (if execution failed)
Test cases are executed individually using the same compilation and
execution pipeline as |:CP-run|, but with isolated input/output for
precise failure analysis.
FILE STRUCTURE *cp-files*
cp.nvim creates the following file structure upon problem setup:

View file

@ -5,14 +5,15 @@
---@field problems Problem[]
---@field scraped_at string
---@field expires_at? number
---@field test_cases? TestCase[]
---@field test_cases? CachedTestCase[]
---@field test_cases_cached_at? number
---@class Problem
---@field id string
---@field name? string
---@class TestCase
---@class CachedTestCase
---@field index? number
---@field input string
---@field output string
@ -146,7 +147,7 @@ end
---@param platform string
---@param contest_id string
---@param problem_id? string
---@return TestCase[]?
---@return CachedTestCase[]?
function M.get_test_cases(platform, contest_id, problem_id)
vim.validate({
platform = { platform, "string" },
@ -164,7 +165,7 @@ end
---@param platform string
---@param contest_id string
---@param problem_id? string
---@param test_cases TestCase[]
---@param test_cases CachedTestCase[]
function M.set_test_cases(platform, contest_id, problem_id, test_cases)
vim.validate({
platform = { platform, "string" },

View file

@ -48,7 +48,7 @@
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
local M = {}
local languages = require("cp.languages")
local constants = require("cp.constants")
---@type cp.Config
M.defaults = {
@ -83,9 +83,21 @@ function M.setup(user_config)
if user_config.hooks then
vim.validate({
before_run = { user_config.hooks.before_run, { "function", "nil" }, true },
before_debug = { user_config.hooks.before_debug, { "function", "nil" }, true },
setup_code = { user_config.hooks.setup_code, { "function", "nil" }, true },
before_run = {
user_config.hooks.before_run,
{ "function", "nil" },
true,
},
before_debug = {
user_config.hooks.before_debug,
{ "function", "nil" },
true,
},
setup_code = {
user_config.hooks.setup_code,
{ "function", "nil" },
true,
},
})
end
@ -94,14 +106,14 @@ function M.setup(user_config)
for lang_name, lang_config in pairs(contest_config) do
if type(lang_config) == "table" and lang_config.extension then
if
not vim.tbl_contains(vim.tbl_keys(languages.filetype_to_language), lang_config.extension)
not vim.tbl_contains(vim.tbl_keys(constants.filetype_to_language), lang_config.extension)
then
error(
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
lang_config.extension,
lang_name,
contest_name,
table.concat(vim.tbl_keys(languages.filetype_to_language), ", ")
table.concat(vim.tbl_keys(constants.filetype_to_language), ", ")
)
)
end
@ -125,7 +137,7 @@ local function default_filename(contest_id, problem_id)
})
if problem_id then
return problem_id:lower()
return (contest_id .. problem_id):lower()
else
return contest_id:lower()
end

View file

@ -1,5 +1,8 @@
local M = {}
M.PLATFORMS = { "atcoder", "codeforces", "cses" }
M.ACTIONS = { "run", "debug", "test", "next", "prev" }
M.CPP = "cpp"
M.PYTHON = "python"

View file

@ -8,8 +8,8 @@
local M = {}
local logger = require("cp.log")
local languages = require("cp.languages")
local filetype_to_language = languages.filetype_to_language
local constants = require("cp.constants")
local filetype_to_language = constants.filetype_to_language
---@param source_file string
---@param contest_config table
@ -89,7 +89,7 @@ end
---@param language_config table
---@param substitutions table<string, string>
---@return {code: integer, stderr: string}
local function compile_generic(language_config, substitutions)
function M.compile_generic(language_config, substitutions)
vim.validate({
language_config = { language_config, "table" },
substitutions = { substitutions, "table" },
@ -210,8 +210,40 @@ local function format_output(exec_result, expected_file, is_debug)
end
---@param ctx ProblemContext
---@param contest_config table
---@param is_debug boolean
---@param contest_config ContestConfig
---@return boolean success
function M.compile_problem(ctx, contest_config)
vim.validate({
ctx = { ctx, "table" },
contest_config = { contest_config, "table" },
})
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
logger.log("No configuration for language: " .. language, vim.log.levels.ERROR)
return false
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
if language_config.compile then
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
logger.log("compilation failed: " .. (compile_result.stderr or "unknown error"), vim.log.levels.ERROR)
return false
end
logger.log("compilation successful")
end
return true
end
function M.run_problem(ctx, contest_config, is_debug)
vim.validate({
ctx = { ctx, "table" },
@ -237,7 +269,7 @@ function M.run_problem(ctx, contest_config, is_debug)
local compile_cmd = is_debug and language_config.debug or language_config.compile
if compile_cmd then
local compile_result = compile_generic(language_config, substitutions)
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
vim.fn.writefile({ compile_result.stderr }, ctx.output_file)
return

View file

@ -27,10 +27,12 @@ local state = {
saved_session = nil,
test_cases = nil,
test_states = {},
test_panel_active = false,
}
local platforms = { "atcoder", "codeforces", "cses" }
local actions = { "run", "debug", "next", "prev" }
local constants = require("cp.constants")
local platforms = constants.PLATFORMS
local actions = constants.ACTIONS
local function set_platform(platform)
if not vim.tbl_contains(platforms, platform) then
@ -93,11 +95,15 @@ local function setup_problem(contest_id, problem_id, language)
end
vim.cmd.e(scrape_ctx.source_file)
local source_buf = vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_lines(0, 0, -1, true)[1] == "" then
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == "" then
local has_luasnip, luasnip = pcall(require, "luasnip")
if has_luasnip then
local prefixed_trigger = ("cp.nvim/%s.%s"):format(state.platform, language)
local filetype = vim.api.nvim_get_option_value("filetype", { buf = source_buf })
local language_name = constants.filetype_to_language[filetype]
local canonical_language = constants.canonical_filetypes[language_name] or language_name
local prefixed_trigger = ("cp.nvim/%s.%s"):format(state.platform, canonical_language)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger })
vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger })
@ -123,12 +129,12 @@ local function setup_problem(contest_id, problem_id, language)
config.hooks.setup_code(ctx)
end
local source_buf = vim.api.nvim_get_current_buf()
local src_buf = vim.api.nvim_get_current_buf()
local input_buf = vim.fn.bufnr(ctx.input_file, true)
local output_buf = vim.fn.bufnr(ctx.output_file, true)
local tile_fn = config.tile or window.default_tile
tile_fn(source_buf, input_buf, output_buf)
tile_fn(src_buf, input_buf, output_buf)
logger.log(("switched to problem %s"):format(ctx.problem_name))
end
@ -192,6 +198,140 @@ local function debug_problem()
end)
end
local function toggle_test_panel()
if state.test_panel_active then
if state.saved_session then
vim.cmd(("source %s"):format(state.saved_session))
vim.fn.delete(state.saved_session)
state.saved_session = nil
end
state.test_panel_active = false
logger.log("test panel closed")
return
end
if state.platform == "codeforces" then
logger.log("test panel not yet supported for codeforces", vim.log.levels.ERROR)
return
end
local problem_id = get_current_problem()
if not problem_id then
return
end
local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config)
local test_module = require("cp.test")
if not test_module.load_test_cases(ctx, state) then
logger.log("no test cases found", vim.log.levels.WARN)
return
end
state.saved_session = vim.fn.tempname()
vim.cmd(("mksession! %s"):format(state.saved_session))
vim.cmd("silent only")
local test_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(test_buf)
vim.bo.filetype = "cptest"
vim.bo.bufhidden = "wipe"
local function refresh_test_panel()
if not test_buf or not vim.api.nvim_buf_is_valid(test_buf) then
return
end
local test_state = test_module.get_test_panel_state()
local test_lines = {}
for i, test_case in ipairs(test_state.test_cases) do
local status_text = test_case.status == "pending" and "?" or string.upper(test_case.status)
local prefix = i == test_state.current_index and "> " or " "
local line = string.format("%s%d %s", prefix, i, status_text)
table.insert(test_lines, line)
end
if test_state.test_cases[test_state.current_index] then
local current_test = test_state.test_cases[test_state.current_index]
table.insert(test_lines, "")
table.insert(test_lines, string.format("── Test %d ──", test_state.current_index))
table.insert(test_lines, "Input:")
for _, line in ipairs(vim.split(current_test.input, "\n", { plain = true, trimempty = true })) do
table.insert(test_lines, line)
end
table.insert(test_lines, "Expected:")
for _, line in ipairs(vim.split(current_test.expected, "\n", { plain = true, trimempty = true })) do
table.insert(test_lines, line)
end
if current_test.actual then
table.insert(test_lines, "Actual:")
for _, line in ipairs(vim.split(current_test.actual, "\n", { plain = true, trimempty = true })) do
table.insert(test_lines, line)
end
end
end
table.insert(test_lines, "")
table.insert(test_lines, "[j/k] Navigate [Enter] Run all tests [q] Close")
vim.api.nvim_buf_set_lines(test_buf, 0, -1, false, test_lines)
end
local function navigate_test_case(delta)
local test_state = test_module.get_test_panel_state()
if #test_state.test_cases == 0 then
return
end
test_state.current_index = test_state.current_index + delta
if test_state.current_index < 1 then
test_state.current_index = #test_state.test_cases
elseif test_state.current_index > #test_state.test_cases then
test_state.current_index = 1
end
refresh_test_panel()
end
local function run_all_tests()
local problem_ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config)
local contest_config = config.contests[state.platform]
local test_state = test_module.get_test_panel_state()
if test_state.test_cases and #test_state.test_cases > 0 then
if not execute.compile_problem(problem_ctx, contest_config) then
return
end
test_module.run_all_test_cases(problem_ctx, contest_config)
refresh_test_panel()
end
end
vim.keymap.set("n", "j", function()
navigate_test_case(1)
end, { buffer = test_buf, silent = true })
vim.keymap.set("n", "k", function()
navigate_test_case(-1)
end, { buffer = test_buf, silent = true })
vim.keymap.set("n", "<CR>", function()
run_all_tests()
end, { buffer = test_buf, silent = true })
vim.keymap.set("n", "q", function()
toggle_test_panel()
end, { buffer = test_buf, silent = true })
refresh_test_panel()
state.test_panel_active = true
local test_state = test_module.get_test_panel_state()
logger.log(string.format("test panel opened (%d test cases)", #test_state.test_cases))
end
---@param delta number 1 for next, -1 for prev
---@param language? string
local function navigate_problem(delta, language)
@ -286,12 +426,26 @@ local function parse_command(args)
if vim.tbl_contains(platforms, first) then
if #filtered_args == 1 then
return { type = "platform_only", platform = first, language = language }
return {
type = "platform_only",
platform = first,
language = language,
}
elseif #filtered_args == 2 then
if first == "cses" then
return { type = "cses_problem", platform = first, problem = filtered_args[2], language = language }
return {
type = "cses_problem",
platform = first,
problem = filtered_args[2],
language = language,
}
else
return { type = "contest_setup", platform = first, contest = filtered_args[2], language = language }
return {
type = "contest_setup",
platform = first,
contest = filtered_args[2],
language = language,
}
end
elseif #filtered_args == 3 then
return {
@ -326,6 +480,8 @@ function M.handle_command(opts)
run_problem()
elseif cmd.action == "debug" then
debug_problem()
elseif cmd.action == "test" then
toggle_test_panel()
elseif cmd.action == "next" then
navigate_problem(1, cmd.language)
elseif cmd.action == "prev" then

View file

@ -79,9 +79,24 @@ function M.scrape_contest_metadata(platform, contest_id)
local args
if platform == "cses" then
args = { "uv", "run", "--directory", plugin_path, scraper_path, "metadata" }
args = {
"uv",
"run",
"--directory",
plugin_path,
scraper_path,
"metadata",
}
else
args = { "uv", "run", "--directory", plugin_path, scraper_path, "metadata", contest_id }
args = {
"uv",
"run",
"--directory",
plugin_path,
scraper_path,
"metadata",
contest_id,
}
end
local result = vim.system(args, {
@ -133,10 +148,34 @@ function M.scrape_problem(ctx)
ensure_io_directory()
if vim.fn.filereadable(ctx.input_file) == 1 and vim.fn.filereadable(ctx.expected_file) == 1 then
local base_name = vim.fn.fnamemodify(ctx.input_file, ":r")
local test_cases = {}
local i = 1
while true do
local input_file = base_name .. "." .. i .. ".cpin"
local expected_file = base_name .. "." .. i .. ".cpout"
if vim.fn.filereadable(input_file) == 1 and vim.fn.filereadable(expected_file) == 1 then
local input_content = table.concat(vim.fn.readfile(input_file), "\n")
local expected_content = table.concat(vim.fn.readfile(expected_file), "\n")
table.insert(test_cases, {
index = i,
input = input_content,
output = expected_content,
})
i = i + 1
else
break
end
end
return {
success = true,
problem_id = ctx.problem_name,
test_count = 1,
test_count = #test_cases,
test_cases = test_cases,
}
end
@ -161,9 +200,26 @@ function M.scrape_problem(ctx)
local args
if ctx.contest == "cses" then
args = { "uv", "run", "--directory", plugin_path, scraper_path, "tests", ctx.contest_id }
args = {
"uv",
"run",
"--directory",
plugin_path,
scraper_path,
"tests",
ctx.contest_id,
}
else
args = { "uv", "run", "--directory", plugin_path, scraper_path, "tests", ctx.contest_id, ctx.problem_id }
args = {
"uv",
"run",
"--directory",
plugin_path,
scraper_path,
"tests",
ctx.contest_id,
ctx.problem_id,
}
end
local result = vim.system(args, {
@ -194,8 +250,41 @@ function M.scrape_problem(ctx)
end
if data.test_cases and #data.test_cases > 0 then
local combined_input = data.test_cases[1].input:gsub("\r", "")
local combined_output = data.test_cases[1].output:gsub("\r", "")
local base_name = vim.fn.fnamemodify(ctx.input_file, ":r")
for i, test_case in ipairs(data.test_cases) do
local input_file = base_name .. "." .. i .. ".cpin"
local expected_file = base_name .. "." .. i .. ".cpout"
local input_content = test_case.input:gsub("\r", "")
local expected_content = test_case.output:gsub("\r", "")
if ctx.contest == "atcoder" then
input_content = "1\n" .. input_content
end
vim.fn.writefile(vim.split(input_content, "\n", true), input_file)
vim.fn.writefile(vim.split(expected_content, "\n", true), expected_file)
end
local combined_input = data.combined and data.combined.input:gsub("\r", "")
or table.concat(
vim.tbl_map(function(tc)
return tc.input
end, data.test_cases),
"\n"
)
local combined_output = data.combined and data.combined.output:gsub("\r", "")
or table.concat(
vim.tbl_map(function(tc)
return tc.output
end, data.test_cases),
"\n"
)
if ctx.contest == "atcoder" then
combined_input = tostring(#data.test_cases) .. "\n" .. combined_input
end
vim.fn.writefile(vim.split(combined_input, "\n", true), ctx.input_file)
vim.fn.writefile(vim.split(combined_output, "\n", true), ctx.expected_file)

View file

@ -10,8 +10,8 @@ function M.setup(config)
local s, i, fmt = ls.snippet, ls.insert_node, require("luasnip.extras.fmt").fmt
local languages = require("cp.languages")
local filetype_to_language = languages.filetype_to_language
local constants = require("cp.constants")
local filetype_to_language = constants.filetype_to_language
local language_to_filetype = {}
for ext, lang in pairs(filetype_to_language) do
@ -107,7 +107,7 @@ if __name__ == "__main__":
for language, template_set in pairs(template_definitions) do
local snippets = {}
local filetype = languages.canonical_filetypes[language]
local filetype = constants.canonical_filetypes[language]
for contest, template in pairs(template_set) do
local prefixed_trigger = ("cp.nvim/%s.%s"):format(contest, language)

255
lua/cp/test.lua Normal file
View file

@ -0,0 +1,255 @@
---@class TestCase
---@field index number
---@field input string
---@field expected string
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
---@field actual string?
---@field time_ms number?
---@field error string?
---@field selected boolean
---@class TestPanelState
---@field test_cases TestCase[]
---@field current_index number
---@field buffer number?
---@field namespace number?
---@field is_active boolean
---@field saved_layout table?
local M = {}
local logger = require("cp.log")
---@type TestPanelState
local test_panel_state = {
test_cases = {},
current_index = 1,
buffer = nil,
namespace = nil,
is_active = false,
saved_layout = nil,
}
---@param index number
---@param input string
---@param expected string
---@return TestCase
local function create_test_case(index, input, expected)
return {
index = index,
input = input,
expected = expected,
status = "pending",
actual = nil,
time_ms = nil,
error = nil,
selected = true,
}
end
---@param platform string
---@param contest_id string
---@param problem_id string?
---@return TestCase[]
local function parse_test_cases_from_cache(platform, contest_id, problem_id)
local cache = require("cp.cache")
cache.load()
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id)
if not cached_test_cases or #cached_test_cases == 0 then
return {}
end
local test_cases = {}
for i, test_case in ipairs(cached_test_cases) do
local index = test_case.index or i
table.insert(test_cases, create_test_case(index, test_case.input, test_case.output))
end
return test_cases
end
---@param input_file string
---@param expected_file string
---@return TestCase[]
local function parse_test_cases_from_files(input_file, expected_file)
if vim.fn.filereadable(input_file) == 0 or vim.fn.filereadable(expected_file) == 0 then
return {}
end
local base_name = vim.fn.fnamemodify(input_file, ":r")
local test_cases = {}
local i = 1
while true do
local individual_input_file = base_name .. "." .. i .. ".cpin"
local individual_expected_file = base_name .. "." .. i .. ".cpout"
if vim.fn.filereadable(individual_input_file) == 1 and vim.fn.filereadable(individual_expected_file) == 1 then
local input_content = table.concat(vim.fn.readfile(individual_input_file), "\n")
local expected_content = table.concat(vim.fn.readfile(individual_expected_file), "\n")
table.insert(test_cases, create_test_case(i, input_content, expected_content))
i = i + 1
else
break
end
end
if #test_cases == 0 then
local input_content = table.concat(vim.fn.readfile(input_file), "\n")
local expected_content = table.concat(vim.fn.readfile(expected_file), "\n")
return { create_test_case(1, input_content, expected_content) }
end
return test_cases
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@param test_case TestCase
---@return table
local function run_single_test_case(ctx, contest_config, test_case)
local language = vim.fn.fnamemodify(ctx.source_file, ":e")
local constants = require("cp.constants")
local language_name = constants.filetype_to_language[language] or contest_config.default_language
local language_config = contest_config[language_name]
if not language_config then
return {
status = "fail",
actual = "",
error = "No language configuration",
time_ms = 0,
}
end
local function substitute_template(cmd_template, substitutions)
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub("{" .. key .. "}", value)
end
table.insert(result, substituted)
end
return result
end
local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version or ""),
}
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
logger.log("binary not found, compiling first...")
local compile_cmd = substitute_template(language_config.compile, substitutions)
local compile_result = vim.system(compile_cmd, { text = true }):wait()
if compile_result.code ~= 0 then
return {
status = "fail",
actual = "",
error = "Compilation failed: " .. (compile_result.stderr or "Unknown error"),
time_ms = 0,
}
end
end
local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
local start_time = vim.uv.hrtime()
local result = vim.system(run_cmd, {
stdin = test_case.input .. "\n",
timeout = contest_config.timeout_ms or 2000,
text = true,
}):wait()
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
local actual_output = (result.stdout or ""):gsub("\n$", "")
local expected_output = test_case.expected:gsub("\n$", "")
local matches = actual_output == expected_output
local status
if result.code == 143 or result.code == 124 then
status = "timeout"
elseif result.code == 0 and matches then
status = "pass"
else
status = "fail"
end
return {
status = status,
actual = actual_output,
error = result.code ~= 0 and result.stderr or nil,
time_ms = execution_time,
}
end
---@param ctx ProblemContext
---@param state table
---@return boolean
function M.load_test_cases(ctx, state)
local test_cases = parse_test_cases_from_cache(state.platform, state.contest_id, state.problem_id)
if #test_cases == 0 then
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
end
test_panel_state.test_cases = test_cases
test_panel_state.current_index = 1
logger.log(("loaded %d test case(s)"):format(#test_cases))
return #test_cases > 0
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@param index number
---@return boolean
function M.run_test_case(ctx, contest_config, index)
local test_case = test_panel_state.test_cases[index]
if not test_case then
return false
end
logger.log(("running test case %d"):format(index))
test_case.status = "running"
local result = run_single_test_case(ctx, contest_config, test_case)
test_case.status = result.status
test_case.actual = result.actual
test_case.error = result.error
test_case.time_ms = result.time_ms
return true
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@return TestCase[]
function M.run_all_test_cases(ctx, contest_config)
local results = {}
for i, _ in ipairs(test_panel_state.test_cases) do
M.run_test_case(ctx, contest_config, i)
table.insert(results, test_panel_state.test_cases[i])
end
return results
end
---@return TestPanelState
function M.get_test_panel_state()
return test_panel_state
end
return M

View file

@ -10,15 +10,7 @@
---@field height integer
local M = {}
local languages = require("cp.languages")
function M.clearcol()
vim.api.nvim_set_option_value("number", false, { scope = "local" })
vim.api.nvim_set_option_value("relativenumber", false, { scope = "local" })
vim.api.nvim_set_option_value("statuscolumn", "", { scope = "local" })
vim.api.nvim_set_option_value("signcolumn", "no", { scope = "local" })
vim.api.nvim_set_option_value("foldcolumn", "0", { scope = "local" })
end
local constants = require("cp.constants")
---@return WindowState
function M.save_layout()
@ -79,7 +71,7 @@ function M.restore_layout(state, tile_fn)
local source_file
if source_files ~= "" then
local files = vim.split(source_files, "\n")
local valid_extensions = vim.tbl_keys(languages.filetype_to_language)
local valid_extensions = vim.tbl_keys(constants.filetype_to_language)
for _, file in ipairs(files) do
local ext = vim.fn.fnamemodify(file, ":e")
if vim.tbl_contains(valid_extensions, ext) then
@ -136,12 +128,10 @@ local function default_tile(source_buf, input_buf, output_buf)
vim.cmd.vsplit()
vim.api.nvim_set_current_buf(output_buf)
vim.bo.filetype = "cp"
M.clearcol()
vim.cmd(("vertical resize %d"):format(math.floor(vim.o.columns * 0.3)))
vim.cmd.split()
vim.api.nvim_set_current_buf(input_buf)
vim.bo.filetype = "cp"
M.clearcol()
vim.cmd.wincmd("h")
end

View file

@ -3,8 +3,9 @@ if vim.g.loaded_cp then
end
vim.g.loaded_cp = 1
local platforms = { "atcoder", "codeforces", "cses" }
local actions = { "run", "debug", "next", "prev" }
local constants = require("cp.constants")
local platforms = constants.PLATFORMS
local actions = constants.ACTIONS
vim.api.nvim_create_user_command("CP", function(opts)
local cp = require("cp")
@ -13,8 +14,7 @@ end, {
nargs = "*",
desc = "Competitive programming helper",
complete = function(ArgLead, CmdLine, _)
local languages_module = require("cp.languages")
local languages = vim.tbl_keys(languages_module.canonical_filetypes)
local languages = vim.tbl_keys(constants.canonical_filetypes)
if ArgLead:match("^--lang=") then
local lang_completions = {}
@ -45,10 +45,10 @@ end, {
if num_args == 2 then
local candidates = { "--lang" }
vim.list_extend(candidates, actions)
local cp = require("cp")
local context = cp.get_current_context()
if context.platform and context.contest_id then
vim.list_extend(candidates, actions)
local cache = require("cp.cache")
cache.load()
local contest_data = cache.get_contest_data(context.platform, context.contest_id)

View file

@ -69,7 +69,5 @@ follows:
- finer-tuned problem limits (i.e. per-problem codeforces time, memory)
- better highlighting
- test case management
- USACO support
- new video with functionality, notify discord members
- note that codeforces support is scuffed: https://codeforces.com/blog/entry/146423
- codeforces: use round number & api not the contest id

View file

@ -169,24 +169,21 @@ def main() -> None:
print(json.dumps(result))
sys.exit(1)
test_cases: list[dict[str, str]] = []
for input_data, output_data in tests:
test_cases.append({"input": input_data, "output": output_data})
if test_cases:
combined_input: str = (
str(len(test_cases))
+ "\n"
+ "\n".join(tc["input"] for tc in test_cases)
individual_test_cases: list[dict[str, str]] = []
for index, (input_data, output_data) in enumerate(tests, 1):
individual_test_cases.append(
{"index": index, "input": input_data, "output": output_data}
)
combined_output: str = "\n".join(tc["output"] for tc in test_cases)
test_cases = [{"input": combined_input, "output": combined_output}]
combined_input = "\n".join(tc["input"] for tc in individual_test_cases)
combined_output = "\n".join(tc["output"] for tc in individual_test_cases)
result = {
"success": True,
"problem_id": problem_id,
"url": url,
"test_cases": test_cases,
"test_cases": individual_test_cases,
"combined": {"input": combined_input, "output": combined_output},
}
print(json.dumps(result))