Merge pull request #121 from barrett-ruth/refactor/picker-fixes
Refactor/picker fixes
This commit is contained in:
commit
35ccd6e217
36 changed files with 466 additions and 4186 deletions
|
|
@ -1,7 +1,5 @@
|
|||
# cp.nvim
|
||||
|
||||
> ⚠️ **Warning**: as of 27/09/25, CodeForces upgraded their anti-scraping technology and support is thus (temporarily) broken. I am actively researching a way around this.
|
||||
|
||||
**The definitive competitive programming environment for Neovim**
|
||||
|
||||
Scrape problems, run tests, and debug solutions across multiple platforms with zero configuration.
|
||||
|
|
@ -29,7 +27,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
|||
### Basic Usage
|
||||
|
||||
1. **Find a contest or problem** on the judge website
|
||||
2. **Set up locally** with `:CP <platform> <contest> [<problem>]`
|
||||
2. **Set up locally** with `:CP <platform> <contest> [--{lang=<lang>,debug}]`
|
||||
|
||||
```
|
||||
:CP codeforces 1848
|
||||
|
|
|
|||
|
|
@ -56,13 +56,6 @@ COMMANDS *cp-commands*
|
|||
:CP {platform} Platform setup: set platform only.
|
||||
Example: >
|
||||
:CP cses
|
||||
<
|
||||
:CP {problem_id} [--lang={language}]
|
||||
Problem switch: switch to different problem
|
||||
within current contest context.
|
||||
Example: >
|
||||
:CP b
|
||||
:CP b --lang=python
|
||||
<
|
||||
Action Commands ~
|
||||
:CP run [--debug] Toggle run panel for individual test case
|
||||
|
|
@ -82,6 +75,16 @@ COMMANDS *cp-commands*
|
|||
:CP prev Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
|
||||
Cache Commands ~
|
||||
:CP cache clear [contest]
|
||||
Clear the cache data (contest list, problem
|
||||
data, file states) for the specified contest,
|
||||
or all contests if none specified
|
||||
|
||||
:CP cache read
|
||||
View the cache in a pretty-printed lua buffer.
|
||||
Exit with q.
|
||||
|
||||
Command Flags ~
|
||||
*cp-flags*
|
||||
Flags can be used with setup and action commands:
|
||||
|
|
@ -151,7 +154,6 @@ Here's an example configuration with lazy.nvim: >lua
|
|||
diff_mode = 'vim',
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
toggle_diff_key = '<c-q>',
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
|
|
@ -229,7 +231,6 @@ is required:
|
|||
{next_test_key} (string, default: "<c-n>") Key to navigate to next test case.
|
||||
{prev_test_key} (string, default: "<c-p>") Key to navigate to previous test case.
|
||||
{toggle_diff_key} (string, default: "<c-t>") Key to cycle through diff modes.
|
||||
{close_key} (string, default: "<c-q>") Close the run panel/interactive terminal
|
||||
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
||||
|
||||
*cp.DiffConfig*
|
||||
|
|
@ -285,7 +286,6 @@ URL format: https://atcoder.jp/contests/abc123/tasks/abc123_a
|
|||
Usage examples: >
|
||||
:CP atcoder abc324 a " Full setup: problem A from contest ABC324
|
||||
:CP atcoder abc324 " Contest setup: load contest metadata only
|
||||
:CP b " Switch to problem B (if contest loaded)
|
||||
:CP next " Navigate to next problem in contest
|
||||
<
|
||||
Note: AtCoder template includes optimizations
|
||||
|
|
@ -303,7 +303,6 @@ URL format: https://codeforces.com/contest/1234/problem/A
|
|||
Usage examples: >
|
||||
:CP codeforces 1934 a " Full setup: problem A from contest 1934
|
||||
:CP codeforces 1934 " Contest setup: load contest metadata only
|
||||
:CP c " Switch to problem C (if contest loaded)
|
||||
:CP prev " Navigate to previous problem in contest
|
||||
<
|
||||
Note: Problem IDs are automatically converted
|
||||
|
|
@ -535,9 +534,9 @@ RUN PANEL KEYMAPS *cp-test-keys*
|
|||
run_panel.next_test_key)
|
||||
<c-p> Navigate to previous test case (configurable via
|
||||
run_panel.prev_test_key)
|
||||
<c-t> Cycle through diff modes: none → git → vim (configurable
|
||||
via run_panel.toggle_diff_key)
|
||||
<c-q> Exit run panel/interactive terminal and restore layout
|
||||
t Cycle through diff modes: none → git → vim
|
||||
q Exit run panel and restore layout
|
||||
<c-q> Exit interactive terminal and restore layout
|
||||
|
||||
Diff Modes ~
|
||||
|
||||
|
|
|
|||
157
lua/cp/cache.lua
157
lua/cp/cache.lua
|
|
@ -11,13 +11,10 @@
|
|||
|
||||
---@class ContestListData
|
||||
---@field contests table[]
|
||||
---@field cached_at number
|
||||
|
||||
---@class ContestData
|
||||
---@field problems Problem[]
|
||||
---@field scraped_at string
|
||||
---@field test_cases? CachedTestCase[]
|
||||
---@field test_cases_cached_at? number
|
||||
---@field timeout_ms? number
|
||||
---@field memory_mb? number
|
||||
---@field interactive? boolean
|
||||
|
|
@ -34,6 +31,7 @@
|
|||
|
||||
local M = {}
|
||||
|
||||
local logger = require('cp.log')
|
||||
local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json'
|
||||
local cache_data = {}
|
||||
local loaded = false
|
||||
|
|
@ -44,7 +42,7 @@ function M.load()
|
|||
end
|
||||
|
||||
if vim.fn.filereadable(cache_file) == 0 then
|
||||
cache_data = {}
|
||||
vim.fn.writefile({}, cache_file)
|
||||
loaded = true
|
||||
return
|
||||
end
|
||||
|
|
@ -60,28 +58,19 @@ function M.load()
|
|||
if ok then
|
||||
cache_data = decoded
|
||||
else
|
||||
cache_data = {}
|
||||
logger.log('Could not decode json in cache file', vim.log.levels.ERROR)
|
||||
end
|
||||
loaded = true
|
||||
end
|
||||
|
||||
function M.save()
|
||||
local ok, _ = pcall(vim.fn.mkdir, vim.fn.fnamemodify(cache_file, ':h'), 'p')
|
||||
if not ok then
|
||||
vim.schedule(function()
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
|
||||
end)
|
||||
return
|
||||
end
|
||||
vim.schedule(function()
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
|
||||
|
||||
local encoded = vim.json.encode(cache_data)
|
||||
local lines = vim.split(encoded, '\n')
|
||||
local write_ok, _ = pcall(vim.fn.writefile, lines, cache_file)
|
||||
if not write_ok then
|
||||
vim.schedule(function()
|
||||
vim.fn.writefile(lines, cache_file)
|
||||
end)
|
||||
end
|
||||
local encoded = vim.json.encode(cache_data)
|
||||
local lines = vim.split(encoded, '\n')
|
||||
vim.fn.writefile(lines, cache_file)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
|
|
@ -98,7 +87,7 @@ function M.get_contest_data(platform, contest_id)
|
|||
end
|
||||
|
||||
local contest_data = cache_data[platform][contest_id]
|
||||
if not contest_data then
|
||||
if not contest_data or vim.tbl_isempty(contest_data) then
|
||||
return nil
|
||||
end
|
||||
|
||||
|
|
@ -115,15 +104,37 @@ function M.set_contest_data(platform, contest_id, problems)
|
|||
problems = { problems, 'table' },
|
||||
})
|
||||
|
||||
if not cache_data[platform] then
|
||||
cache_data[platform] = {}
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
local existing = cache_data[platform][contest_id] or {}
|
||||
|
||||
local existing_by_id = {}
|
||||
if existing.problems then
|
||||
for _, p in ipairs(existing.problems) do
|
||||
existing_by_id[p.id] = p
|
||||
end
|
||||
end
|
||||
|
||||
cache_data[platform][contest_id] = {
|
||||
problems = problems,
|
||||
scraped_at = os.date('%Y-%m-%d'),
|
||||
}
|
||||
local merged = {}
|
||||
for _, p in ipairs(problems) do
|
||||
local prev = existing_by_id[p.id] or {}
|
||||
local merged_p = {
|
||||
id = p.id,
|
||||
name = p.name or prev.name,
|
||||
test_cases = prev.test_cases,
|
||||
timeout_ms = prev.timeout_ms,
|
||||
memory_mb = prev.memory_mb,
|
||||
interactive = prev.interactive,
|
||||
}
|
||||
table.insert(merged, merged_p)
|
||||
end
|
||||
|
||||
existing.problems = merged
|
||||
existing.index_map = {}
|
||||
for i, p in ipairs(merged) do
|
||||
existing.index_map[p.id] = i
|
||||
end
|
||||
|
||||
cache_data[platform][contest_id] = existing
|
||||
M.save()
|
||||
end
|
||||
|
||||
|
|
@ -152,16 +163,23 @@ function M.get_test_cases(platform, contest_id, problem_id)
|
|||
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
|
||||
if
|
||||
not cache_data[platform]
|
||||
or not cache_data[platform][contest_id]
|
||||
or not cache_data[platform][contest_id].problems
|
||||
or not cache_data[platform][contest_id].index_map
|
||||
then
|
||||
print('bad, failing')
|
||||
return nil
|
||||
end
|
||||
return cache_data[platform][problem_key].test_cases
|
||||
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
return cache_data[platform][contest_id].problems[index].test_cases
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param problem_id string
|
||||
---@param test_cases CachedTestCase[]
|
||||
---@param timeout_ms? number
|
||||
---@param memory_mb? number
|
||||
|
|
@ -185,22 +203,12 @@ function M.set_test_cases(
|
|||
interactive = { interactive, { 'boolean', 'nil' }, true },
|
||||
})
|
||||
|
||||
local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
|
||||
if not cache_data[platform] then
|
||||
cache_data[platform] = {}
|
||||
end
|
||||
if not cache_data[platform][problem_key] then
|
||||
cache_data[platform][problem_key] = {}
|
||||
end
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
|
||||
cache_data[platform][contest_id].problems[index].test_cases = test_cases
|
||||
cache_data[platform][contest_id].problems[index].timeout_ms = timeout_ms or 0
|
||||
cache_data[platform][contest_id].problems[index].memory_mb = memory_mb or 0
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -215,12 +223,9 @@ function M.get_constraints(platform, contest_id, problem_id)
|
|||
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 index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
|
||||
local problem_data = cache_data[platform][problem_key]
|
||||
local problem_data = cache_data[platform][contest_id].problems[index]
|
||||
return problem_data.timeout_ms, problem_data.memory_mb
|
||||
end
|
||||
|
||||
|
|
@ -255,43 +260,34 @@ function M.set_file_state(file_path, platform, contest_id, problem_id, language)
|
|||
end
|
||||
|
||||
---@param platform string
|
||||
---@return table[]?
|
||||
---@return table[]
|
||||
function M.get_contest_list(platform)
|
||||
if not cache_data.contest_lists or not cache_data.contest_lists[platform] then
|
||||
return nil
|
||||
local contest_list = {}
|
||||
for contest_id, contest_data in pairs(cache_data[platform] or {}) do
|
||||
table.insert(contest_list, {
|
||||
id = contest_id,
|
||||
name = contest_data.name,
|
||||
display_name = contest_data.display_name,
|
||||
})
|
||||
end
|
||||
|
||||
return cache_data.contest_lists[platform].contests
|
||||
return contest_list
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contests table[]
|
||||
function M.set_contest_list(platform, contests)
|
||||
if not cache_data.contest_lists then
|
||||
cache_data.contest_lists = {}
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
for _, contest in ipairs(contests) do
|
||||
cache_data[platform][contest.id] = cache_data[platform][contest] or {}
|
||||
cache_data[platform][contest.id].display_name = contest.display_name
|
||||
cache_data[platform][contest.id].name = contest.name
|
||||
end
|
||||
|
||||
cache_data.contest_lists[platform] = {
|
||||
contests = contests,
|
||||
cached_at = os.time(),
|
||||
}
|
||||
|
||||
M.save()
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
function M.clear_contest_list(platform)
|
||||
if cache_data.contest_lists and cache_data.contest_lists[platform] then
|
||||
cache_data.contest_lists[platform] = nil
|
||||
M.save()
|
||||
end
|
||||
end
|
||||
|
||||
function M.clear_all()
|
||||
cache_data = {
|
||||
file_states = {},
|
||||
contest_lists = {},
|
||||
}
|
||||
cache_data = {}
|
||||
M.save()
|
||||
end
|
||||
|
||||
|
|
@ -300,10 +296,15 @@ function M.clear_platform(platform)
|
|||
if cache_data[platform] then
|
||||
cache_data[platform] = nil
|
||||
end
|
||||
if cache_data.contest_lists and cache_data.contest_lists[platform] then
|
||||
cache_data.contest_lists[platform] = nil
|
||||
end
|
||||
|
||||
M.save()
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.get_data_pretty()
|
||||
M.load()
|
||||
|
||||
return vim.inspect(cache_data)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -7,15 +7,46 @@ local logger = require('cp.log')
|
|||
local platforms = constants.PLATFORMS
|
||||
|
||||
function M.handle_cache_command(cmd)
|
||||
if cmd.subcommand == 'clear' then
|
||||
if cmd.subcommand == 'read' then
|
||||
local data = cache.get_data_pretty()
|
||||
local name = 'cp.nvim://cache.lua'
|
||||
|
||||
local existing = vim.fn.bufnr(name)
|
||||
local buf
|
||||
if existing ~= -1 then
|
||||
buf = existing
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(data, '\n'))
|
||||
else
|
||||
buf = vim.api.nvim_create_buf(true, true)
|
||||
vim.api.nvim_buf_set_name(buf, name)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(data, '\n'))
|
||||
vim.bo[buf].filetype = 'lua'
|
||||
vim.bo[buf].buftype = 'nofile'
|
||||
vim.bo[buf].bufhidden = 'wipe'
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.api.nvim_buf_set_keymap(
|
||||
buf,
|
||||
'n',
|
||||
'q',
|
||||
'<cmd>bd!<cr>',
|
||||
{ nowait = true, noremap = true, silent = true }
|
||||
)
|
||||
end
|
||||
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
elseif cmd.subcommand == 'clear' then
|
||||
cache.load()
|
||||
if cmd.platform then
|
||||
if vim.tbl_contains(platforms, cmd.platform) then
|
||||
cache.clear_platform(cmd.platform)
|
||||
logger.log(('cleared cache for %s'):format(cmd.platform), vim.log.levels.INFO, true)
|
||||
logger.log(
|
||||
('Cache cleared for platform %s'):format(cmd.platform),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
else
|
||||
logger.log(
|
||||
('unknown platform: %s. Available: %s'):format(
|
||||
("Unknown platform: '%s'. Available: %s"):format(
|
||||
cmd.platform,
|
||||
table.concat(platforms, ', ')
|
||||
),
|
||||
|
|
@ -24,7 +55,7 @@ function M.handle_cache_command(cmd)
|
|||
end
|
||||
else
|
||||
cache.clear_all()
|
||||
logger.log('cleared all cache', vim.log.levels.INFO, true)
|
||||
logger.log('Cache cleared', vim.log.levels.INFO, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ local platforms = constants.PLATFORMS
|
|||
local actions = constants.ACTIONS
|
||||
|
||||
local function parse_command(args)
|
||||
if #args == 0 then
|
||||
if vim.tbl_isempty(args) then
|
||||
return {
|
||||
type = 'restore_from_file',
|
||||
}
|
||||
|
|
@ -44,11 +44,11 @@ local function parse_command(args)
|
|||
if not subcommand then
|
||||
return { type = 'error', message = 'cache command requires subcommand: clear' }
|
||||
end
|
||||
if subcommand == 'clear' then
|
||||
if vim.tbl_contains({ 'clear', 'read' }, subcommand) then
|
||||
local platform = filtered_args[3]
|
||||
return {
|
||||
type = 'cache',
|
||||
subcommand = 'clear',
|
||||
subcommand = subcommand,
|
||||
platform = platform,
|
||||
}
|
||||
else
|
||||
|
|
@ -62,9 +62,8 @@ 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,
|
||||
type = 'error',
|
||||
message = 'Too few arguments - specify a contest.',
|
||||
}
|
||||
elseif #filtered_args == 2 then
|
||||
return {
|
||||
|
|
@ -75,11 +74,8 @@ local function parse_command(args)
|
|||
}
|
||||
elseif #filtered_args == 3 then
|
||||
return {
|
||||
type = 'full_setup',
|
||||
platform = first,
|
||||
contest = filtered_args[2],
|
||||
problem = filtered_args[3],
|
||||
language = language,
|
||||
type = 'error',
|
||||
message = 'Setup contests with :CP <platform> <contest_id> [--{lang=<lang>,debug}]',
|
||||
}
|
||||
else
|
||||
return { type = 'error', message = 'Too many arguments' }
|
||||
|
|
@ -89,16 +85,6 @@ local function parse_command(args)
|
|||
if state.get_platform() and state.get_contest_id() then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data =
|
||||
cache.get_contest_data(state.get_platform() or '', state.get_contest_id() or '')
|
||||
if contest_data and contest_data.problems then
|
||||
local problem_ids = vim.tbl_map(function(prob)
|
||||
return prob.id
|
||||
end, contest_data.problems)
|
||||
if vim.tbl_contains(problem_ids, first) then
|
||||
return { type = 'problem_switch', problem = first, language = language }
|
||||
end
|
||||
end
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("invalid subcommand '%s'"):format(first),
|
||||
|
|
@ -147,33 +133,13 @@ function M.handle_command(opts)
|
|||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'platform_only' then
|
||||
local setup = require('cp.setup')
|
||||
setup.set_platform(cmd.platform)
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'contest_setup' then
|
||||
local setup = require('cp.setup')
|
||||
if setup.set_platform(cmd.platform) then
|
||||
setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language)
|
||||
setup.setup_contest(cmd.platform, cmd.contest, cmd.language, nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'full_setup' then
|
||||
local setup = require('cp.setup')
|
||||
if setup.set_platform(cmd.platform) then
|
||||
setup.setup_contest(cmd.platform, cmd.contest, cmd.problem, cmd.language)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.type == 'problem_switch' then
|
||||
local setup = require('cp.setup')
|
||||
setup.setup_problem(state.get_contest_id() or '', cmd.problem, cmd.language)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function M.handle_pick_action()
|
|||
|
||||
if not config.picker then
|
||||
logger.log(
|
||||
'No picker configured. Set picker = "telescope" or picker = "fzf-lua" in config',
|
||||
'No picker configured. Set picker = "{telescope,fzf-lua}" in your config.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -20,14 +20,14 @@ function M.handle_pick_action()
|
|||
local ok = pcall(require, 'telescope')
|
||||
if not ok then
|
||||
logger.log(
|
||||
'Telescope not available. Install telescope.nvim or change picker config',
|
||||
'telescope.nvim is not available. Install telescope.nvim xor change your picker config.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok_cp, telescope_picker = pcall(require, 'cp.pickers.telescope')
|
||||
if not ok_cp then
|
||||
logger.log('Failed to load telescope integration', vim.log.levels.ERROR)
|
||||
logger.log('Failed to load telescope integration.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -36,14 +36,14 @@ function M.handle_pick_action()
|
|||
local ok, _ = pcall(require, 'fzf-lua')
|
||||
if not ok then
|
||||
logger.log(
|
||||
'fzf-lua not available. Install fzf-lua or change picker config',
|
||||
'fzf-lua is not available. Install fzf-lua xor change your picker config',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok_cp, fzf_picker = pcall(require, 'cp.pickers.fzf_lua')
|
||||
if not ok_cp then
|
||||
logger.log('Failed to load fzf-lua integration', vim.log.levels.ERROR)
|
||||
logger.log('Failed to load fzf-lua integration.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@
|
|||
---@field diff_mode "none"|"vim"|"git" Diff backend to use
|
||||
---@field next_test_key string Key to navigate to next test case
|
||||
---@field prev_test_key string Key to navigate to previous test case
|
||||
---@field toggle_diff_key string Key to cycle through diff modes
|
||||
---@field close_key string Key to close panel/interactive terminal
|
||||
---@field max_output_lines number Maximum lines of test output to display
|
||||
|
||||
---@class DiffGitConfig
|
||||
|
|
@ -104,8 +102,6 @@ M.defaults = {
|
|||
diff_mode = 'none',
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
toggle_diff_key = '<c-t>',
|
||||
close_key = '<c-q>',
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
|
|
@ -224,20 +220,6 @@ function M.setup(user_config)
|
|||
end,
|
||||
'prev_test_key must be a non-empty string',
|
||||
},
|
||||
toggle_diff_key = {
|
||||
config.run_panel.toggle_diff_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'toggle_diff_key must be a non-empty string',
|
||||
},
|
||||
close_key = {
|
||||
config.run_panel.close_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'close_key must be a non-empty string',
|
||||
},
|
||||
max_output_lines = {
|
||||
config.run_panel.max_output_lines,
|
||||
function(value)
|
||||
|
|
@ -270,7 +252,7 @@ function M.setup(user_config)
|
|||
end
|
||||
end
|
||||
|
||||
if #available_langs == 0 then
|
||||
if vim.tbl_isemtpy(available_langs) then
|
||||
error('No language configurations found')
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ local logger = require('cp.log')
|
|||
local snippets = require('cp.snippets')
|
||||
|
||||
if not vim.fn.has('nvim-0.10.0') then
|
||||
logger.log('[cp.nvim]: requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||
return {}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ local picker_utils = require('cp.pickers')
|
|||
|
||||
local M = {}
|
||||
|
||||
local function contest_picker(platform)
|
||||
local function contest_picker(platform, refresh)
|
||||
local constants = require('cp.constants')
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
|
||||
local fzf = require('fzf-lua')
|
||||
local contests = picker_utils.get_contests_for_platform(platform)
|
||||
local contests = picker_utils.get_platform_contests(platform, refresh)
|
||||
|
||||
if #contests == 0 then
|
||||
if vim.tbl_isempty(contests) then
|
||||
vim.notify(
|
||||
('No contests found for platform: %s'):format(platform_display_name),
|
||||
vim.log.levels.WARN
|
||||
|
|
@ -27,7 +27,7 @@ local function contest_picker(platform)
|
|||
},
|
||||
actions = {
|
||||
['default'] = function(selected)
|
||||
if not selected or #selected == 0 then
|
||||
if vim.tbl_isempty(selected) then
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ local function contest_picker(platform)
|
|||
['ctrl-r'] = function()
|
||||
local cache = require('cp.cache')
|
||||
cache.clear_contest_list(platform)
|
||||
contest_picker(platform)
|
||||
contest_picker(platform, true)
|
||||
end,
|
||||
},
|
||||
})
|
||||
|
|
@ -65,7 +65,7 @@ function M.pick()
|
|||
prompt = 'Select Platform> ',
|
||||
actions = {
|
||||
['default'] = function(selected)
|
||||
if not selected or #selected == 0 then
|
||||
if vim.tbl_isempty(selected) then
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ local M = {}
|
|||
|
||||
local cache = require('cp.cache')
|
||||
local config = require('cp.config').get_config()
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local utils = require('cp.utils')
|
||||
local scraper = require('cp.scraper')
|
||||
|
||||
---@class cp.PlatformItem
|
||||
---@field id string Platform identifier (e.g. "codeforces", "atcoder", "cses")
|
||||
|
|
@ -21,7 +22,6 @@ local utils = require('cp.utils')
|
|||
|
||||
---@return cp.PlatformItem[]
|
||||
function M.get_platforms()
|
||||
local constants = require('cp.constants')
|
||||
local result = {}
|
||||
|
||||
for _, platform in ipairs(constants.PLATFORMS) do
|
||||
|
|
@ -38,169 +38,35 @@ end
|
|||
|
||||
---Get list of contests for a specific platform
|
||||
---@param platform string Platform identifier (e.g. "codeforces", "atcoder")
|
||||
---@param refresh? boolean Whether to skip caching and append new contests
|
||||
---@return cp.ContestItem[]
|
||||
function M.get_contests_for_platform(platform)
|
||||
logger.log('loading contests...', vim.log.levels.INFO, true)
|
||||
function M.get_platform_contests(platform, refresh)
|
||||
logger.log(
|
||||
('Loading %s contests...'):format(constants.PLATFORM_DISPLAY_NAMES[platform]),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
|
||||
cache.load()
|
||||
local cached_contests = cache.get_contest_list(platform)
|
||||
if cached_contests then
|
||||
return cached_contests
|
||||
|
||||
local picker_contests = cache.get_contest_list(platform)
|
||||
|
||||
if refresh or vim.tbl_isempty(picker_contests) then
|
||||
logger.log(('Cache miss on %s contests'):format(platform))
|
||||
local contests = scraper.scrape_contest_list(platform)
|
||||
|
||||
cache.set_contest_list(platform, contests)
|
||||
end
|
||||
|
||||
if not utils.setup_python_env() then
|
||||
return {}
|
||||
end
|
||||
logger.log(
|
||||
('Loaded %s %s contests.'):format(#picker_contests, constants.PLATFORM_DISPLAY_NAMES[platform]),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local cmd = {
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
plugin_path,
|
||||
'-m',
|
||||
'scrapers.' .. platform,
|
||||
'contests',
|
||||
}
|
||||
picker_contests = cache.get_contest_list(platform)
|
||||
|
||||
local result = vim
|
||||
.system(cmd, {
|
||||
cwd = plugin_path,
|
||||
text = true,
|
||||
timeout = 30000,
|
||||
})
|
||||
:wait()
|
||||
|
||||
logger.log(('exit code: %d, stdout length: %d'):format(result.code, #(result.stdout or '')))
|
||||
if result.stderr and #result.stderr > 0 then
|
||||
logger.log(('stderr: %s'):format(result.stderr:sub(1, 200)))
|
||||
end
|
||||
|
||||
if result.code ~= 0 then
|
||||
logger.log(
|
||||
('Failed to load contests: %s'):format(result.stderr or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return {}
|
||||
end
|
||||
|
||||
logger.log(('stdout preview: %s'):format(result.stdout:sub(1, 100)))
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
logger.log(('JSON parse error: %s'):format(tostring(data)), vim.log.levels.ERROR)
|
||||
return {}
|
||||
end
|
||||
if not data.success then
|
||||
logger.log(
|
||||
('Scraper returned success=false: %s'):format(data.error or 'no error message'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return {}
|
||||
end
|
||||
|
||||
local contests = {}
|
||||
for _, contest in ipairs(data.contests or {}) do
|
||||
table.insert(contests, {
|
||||
id = contest.id,
|
||||
name = contest.name,
|
||||
display_name = contest.display_name,
|
||||
})
|
||||
end
|
||||
|
||||
logger.log(('loaded %d contests'):format(#contests))
|
||||
return contests
|
||||
end
|
||||
|
||||
---@param platform string Platform identifier
|
||||
---@param contest_id string Contest identifier
|
||||
---@return cp.ProblemItem[]
|
||||
function M.get_problems_for_contest(platform, contest_id)
|
||||
logger.log('loading contest problems...', vim.log.levels.INFO, true)
|
||||
|
||||
local problems = {}
|
||||
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if contest_data and contest_data.problems then
|
||||
for _, problem in ipairs(contest_data.problems) do
|
||||
table.insert(problems, {
|
||||
id = problem.id,
|
||||
name = problem.name,
|
||||
display_name = problem.name,
|
||||
})
|
||||
end
|
||||
return problems
|
||||
end
|
||||
|
||||
if not utils.setup_python_env() then
|
||||
return problems
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local cmd = {
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
plugin_path,
|
||||
'-m',
|
||||
'scrapers.' .. platform,
|
||||
'metadata',
|
||||
contest_id,
|
||||
}
|
||||
|
||||
local result = vim
|
||||
.system(cmd, {
|
||||
cwd = plugin_path,
|
||||
text = true,
|
||||
timeout = 30000,
|
||||
})
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
logger.log(
|
||||
('Failed to scrape contest: %s'):format(result.stderr or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return problems
|
||||
end
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
logger.log('Failed to parse contest data', vim.log.levels.ERROR)
|
||||
return problems
|
||||
end
|
||||
if not data.success then
|
||||
logger.log(data.error or 'Contest scraping failed', vim.log.levels.ERROR)
|
||||
return problems
|
||||
end
|
||||
|
||||
if not data.problems or #data.problems == 0 then
|
||||
logger.log('Contest has no problems available', vim.log.levels.WARN)
|
||||
return problems
|
||||
end
|
||||
|
||||
cache.set_contest_data(platform, contest_id, data.problems)
|
||||
|
||||
for _, problem in ipairs(data.problems) do
|
||||
table.insert(problems, {
|
||||
id = problem.id,
|
||||
name = problem.name,
|
||||
display_name = problem.name,
|
||||
})
|
||||
end
|
||||
|
||||
return problems
|
||||
end
|
||||
|
||||
---@param platform string Platform identifier
|
||||
---@param contest_id string Contest identifier
|
||||
---@param problem_id string Problem identifier
|
||||
function M.setup_problem(platform, contest_id, problem_id)
|
||||
vim.schedule(function()
|
||||
local cp = require('cp')
|
||||
cp.handle_command({ fargs = { platform, contest_id, problem_id } })
|
||||
end)
|
||||
return picker_contests
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ local picker_utils = require('cp.pickers')
|
|||
|
||||
local M = {}
|
||||
|
||||
local function contest_picker(opts, platform)
|
||||
local function contest_picker(opts, platform, refresh)
|
||||
local constants = require('cp.constants')
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
|
||||
local contests = picker_utils.get_contests_for_platform(platform)
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
local contests = picker_utils.get_platform_contests(platform, refresh)
|
||||
|
||||
if #contests == 0 then
|
||||
if vim.tbl_isempty(contests) then
|
||||
vim.notify(
|
||||
('No contests found for platform: %s'):format(platform_display_name),
|
||||
vim.log.levels.WARN
|
||||
|
|
@ -48,10 +48,8 @@ local function contest_picker(opts, platform)
|
|||
end)
|
||||
|
||||
map('i', '<c-r>', function()
|
||||
local cache = require('cp.cache')
|
||||
cache.clear_contest_list(platform)
|
||||
actions.close(prompt_bufnr)
|
||||
contest_picker(opts, platform)
|
||||
contest_picker(opts, platform, true)
|
||||
end)
|
||||
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ local state = require('cp.state')
|
|||
function M.restore_from_current_file()
|
||||
local current_file = vim.fn.expand('%:p')
|
||||
if current_file == '' then
|
||||
logger.log('No file is currently open', vim.log.levels.ERROR)
|
||||
logger.log('No file is currently open.', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ function M.restore_from_current_file()
|
|||
local file_state = cache.get_file_state(current_file)
|
||||
if not file_state then
|
||||
logger.log(
|
||||
'No cached state found for current file. Use :CP <platform> <contest> <problem> first.',
|
||||
'No cached state found for current file. Use :CP <platform> <contest> [--{lang=<lang>,debug}...] first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
|
|
@ -25,7 +25,7 @@ function M.restore_from_current_file()
|
|||
('Restoring from cached state: %s %s %s'):format(
|
||||
file_state.platform,
|
||||
file_state.contest_id,
|
||||
file_state.problem_id or 'N/A'
|
||||
file_state.problem_id
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +37,12 @@ function M.restore_from_current_file()
|
|||
state.set_contest_id(file_state.contest_id)
|
||||
state.set_problem_id(file_state.problem_id)
|
||||
|
||||
setup.setup_problem(file_state.contest_id, file_state.problem_id, file_state.language)
|
||||
setup.setup_contest(
|
||||
file_state.platform,
|
||||
file_state.contest_id,
|
||||
file_state.language,
|
||||
file_state.problem_id
|
||||
)
|
||||
|
||||
return true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ end
|
|||
---@return {code: integer, stdout: string, stderr: string}
|
||||
function M.compile_generic(language_config, substitutions)
|
||||
if not language_config.compile then
|
||||
logger.log('no compilation step required')
|
||||
logger.log('No compilation step required for language - skipping.')
|
||||
return { code = 0, stderr = '' }
|
||||
end
|
||||
|
||||
|
|
@ -73,9 +73,9 @@ function M.compile_generic(language_config, substitutions)
|
|||
result.stderr = ansi.bytes_to_string(result.stderr or '')
|
||||
|
||||
if result.code == 0 then
|
||||
logger.log(('compilation successful (%.1fms)'):format(compile_time), vim.log.levels.INFO)
|
||||
logger.log(('Compilation successful in %.1fms.'):format(compile_time), vim.log.levels.INFO)
|
||||
else
|
||||
logger.log(('compilation failed (%.1fms)'):format(compile_time))
|
||||
logger.log(('Compilation failed in %.1fms.'):format(compile_time))
|
||||
end
|
||||
|
||||
return result
|
||||
|
|
@ -107,14 +107,14 @@ local function execute_command(cmd, input_data, timeout_ms)
|
|||
local actual_code = result.code or 0
|
||||
|
||||
if result.code == 124 then
|
||||
logger.log(('execution timed out after %.1fms'):format(execution_time), vim.log.levels.WARN)
|
||||
logger.log(('Execution timed out in %.1fms.'):format(execution_time), vim.log.levels.WARN)
|
||||
elseif actual_code ~= 0 then
|
||||
logger.log(
|
||||
('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
|
||||
('Execution failed in %.1fms (exit code %d).'):format(execution_time, actual_code),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
else
|
||||
logger.log(('execution successful (%.1fms)'):format(execution_time))
|
||||
logger.log(('Execution successful in %.1fms.'):format(execution_time))
|
||||
end
|
||||
|
||||
return {
|
||||
|
|
@ -177,8 +177,8 @@ function M.compile_problem(contest_config, is_debug)
|
|||
local state = require('cp.state')
|
||||
local source_file = state.get_source_file()
|
||||
if not source_file then
|
||||
logger.log('No source file found', vim.log.levels.ERROR)
|
||||
return { success = false, output = 'No source file found' }
|
||||
logger.log('No source file found.', vim.log.levels.ERROR)
|
||||
return { success = false, output = 'No source file found.' }
|
||||
end
|
||||
|
||||
local language = get_language_from_file(source_file, contest_config)
|
||||
|
|
@ -186,7 +186,7 @@ function M.compile_problem(contest_config, is_debug)
|
|||
|
||||
if not language_config then
|
||||
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
|
||||
return { success = false, output = 'No configuration for language: ' .. language }
|
||||
return { success = false, output = ('No configuration for language %s.'):format(language) }
|
||||
end
|
||||
|
||||
local binary_file = state.get_binary_file()
|
||||
|
|
@ -203,10 +203,6 @@ function M.compile_problem(contest_config, is_debug)
|
|||
if compile_result.code ~= 0 then
|
||||
return { success = false, output = compile_result.stdout or 'unknown error' }
|
||||
end
|
||||
logger.log(
|
||||
('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'),
|
||||
vim.log.levels.INFO
|
||||
)
|
||||
end
|
||||
|
||||
return { success = true, output = nil }
|
||||
|
|
@ -220,7 +216,10 @@ function M.run_problem(contest_config, is_debug)
|
|||
local output_file = state.get_output_file()
|
||||
|
||||
if not source_file or not output_file then
|
||||
logger.log('Missing required file paths', vim.log.levels.ERROR)
|
||||
logger.log(
|
||||
('Missing required file paths %s and %s'):format(source_file, output_file),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -257,13 +256,14 @@ function M.run_problem(contest_config, is_debug)
|
|||
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
local expected_file = state.get_expected_file()
|
||||
|
||||
if not platform or not contest_id or not expected_file then
|
||||
logger.log('configure a contest before running a problem', vim.log.levels.ERROR)
|
||||
logger.log('Configure a contest before running a problem', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local timeout_ms, _ = cache.get_constraints(platform, contest_id, problem_id)
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ end
|
|||
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)
|
||||
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id) or {}
|
||||
|
||||
if not cached_test_cases or #cached_test_cases == 0 then
|
||||
if vim.tbl_isempty(cached_test_cases) then
|
||||
return {}
|
||||
end
|
||||
|
||||
|
|
@ -83,34 +83,6 @@ local function parse_test_cases_from_cache(platform, contest_id, problem_id)
|
|||
return test_cases
|
||||
end
|
||||
|
||||
---@param input_file string
|
||||
---@return TestCase[]
|
||||
local function parse_test_cases_from_files(input_file, _)
|
||||
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
|
||||
|
||||
return test_cases
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string?
|
||||
|
|
@ -136,28 +108,11 @@ end
|
|||
local function run_single_test_case(contest_config, cp_config, test_case)
|
||||
local state = require('cp.state')
|
||||
local source_file = state.get_source_file()
|
||||
if not source_file then
|
||||
return {
|
||||
status = 'fail',
|
||||
actual = '',
|
||||
error = 'No source file found',
|
||||
time_ms = 0,
|
||||
}
|
||||
end
|
||||
|
||||
local language = vim.fn.fnamemodify(source_file, ':e')
|
||||
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
|
||||
|
|
@ -185,7 +140,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
|||
}
|
||||
|
||||
if language_config.compile and binary_file and vim.fn.filereadable(binary_file) == 0 then
|
||||
logger.log('binary not found, compiling first...')
|
||||
logger.log('Binary not found - compiling first.')
|
||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
||||
local redirected_cmd = vim.deepcopy(compile_cmd)
|
||||
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
|
||||
|
|
@ -208,6 +163,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
|||
ok = false,
|
||||
signal = nil,
|
||||
timed_out = false,
|
||||
actual_highlights = {},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -219,9 +175,6 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
|||
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 redirected_run_cmd = vim.deepcopy(run_cmd)
|
||||
redirected_run_cmd[#redirected_run_cmd] = redirected_run_cmd[#redirected_run_cmd] .. ' 2>&1'
|
||||
local result = vim
|
||||
|
|
@ -299,13 +252,9 @@ function M.load_test_cases(state)
|
|||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id()
|
||||
)
|
||||
) or {}
|
||||
|
||||
if #test_cases == 0 then
|
||||
local input_file = state.get_input_file()
|
||||
local expected_file = state.get_expected_file()
|
||||
test_cases = parse_test_cases_from_files(input_file, expected_file)
|
||||
end
|
||||
-- TODO: re-fetch/cache-populating mechanism to ge the test cases if not in the cache
|
||||
|
||||
run_panel_state.test_cases = test_cases
|
||||
run_panel_state.current_index = 1
|
||||
|
|
@ -315,14 +264,7 @@ function M.load_test_cases(state)
|
|||
state.get_problem_id()
|
||||
)
|
||||
|
||||
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), vim.log.levels.INFO)
|
||||
logger.log(('Loaded %d test case(s)'):format(#test_cases), vim.log.levels.INFO)
|
||||
return #test_cases > 0
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,42 @@
|
|||
local M = {}
|
||||
local cache = require('cp.cache')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local function run_scraper(platform, subcommand, args, callback)
|
||||
local logger = require('cp.log')
|
||||
|
||||
local function syshandle(result)
|
||||
if result.code ~= 0 then
|
||||
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
return {
|
||||
success = false,
|
||||
error = msg,
|
||||
}
|
||||
end
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
local msg = 'Failed to parse scraper output: ' .. tostring(data)
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
return {
|
||||
success = false,
|
||||
error = msg,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
success = true,
|
||||
data = data,
|
||||
}
|
||||
end
|
||||
|
||||
local function run_scraper(platform, subcommand, args, opts)
|
||||
if not utils.setup_python_env() then
|
||||
callback({ success = false, error = 'Python environment setup failed' })
|
||||
return
|
||||
local msg = 'Python environment setup failed'
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
return {
|
||||
success = false,
|
||||
message = msg,
|
||||
}
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
|
|
@ -18,114 +49,98 @@ local function run_scraper(platform, subcommand, args, callback)
|
|||
'scrapers.' .. platform,
|
||||
subcommand,
|
||||
}
|
||||
vim.list_extend(cmd, args)
|
||||
|
||||
for _, arg in ipairs(args or {}) do
|
||||
table.insert(cmd, arg)
|
||||
end
|
||||
|
||||
vim.system(cmd, {
|
||||
cwd = plugin_path,
|
||||
local sysopts = {
|
||||
text = true,
|
||||
timeout = 30000,
|
||||
}, function(result)
|
||||
if result.code ~= 0 then
|
||||
callback({
|
||||
success = false,
|
||||
error = 'Scraper failed: ' .. (result.stderr or 'Unknown error'),
|
||||
})
|
||||
return
|
||||
end
|
||||
}
|
||||
|
||||
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||
if not ok then
|
||||
callback({
|
||||
success = false,
|
||||
error = 'Failed to parse scraper output: ' .. tostring(data),
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
callback(data)
|
||||
end)
|
||||
if opts.sync then
|
||||
local result = vim.system(cmd, sysopts):wait()
|
||||
return syshandle(result)
|
||||
else
|
||||
vim.system(cmd, sysopts, function(result)
|
||||
return opts.on_exit(syshandle(result))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function M.scrape_contest_metadata(platform, contest_id, callback)
|
||||
cache.load()
|
||||
|
||||
local cached = cache.get_contest_data(platform, contest_id)
|
||||
if cached then
|
||||
callback({ success = true, problems = cached.problems })
|
||||
return
|
||||
end
|
||||
|
||||
run_scraper(platform, 'metadata', { contest_id }, function(result)
|
||||
if result.success and result.problems then
|
||||
cache.set_contest_data(platform, contest_id, result.problems)
|
||||
end
|
||||
callback(result)
|
||||
end)
|
||||
run_scraper(platform, 'metadata', { contest_id }, {
|
||||
on_exit = function(result)
|
||||
if not result or not result.success then
|
||||
logger.log(
|
||||
('Failed to scrape metadata for %s contest %s.'):format(platform, contest_id),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
local data = result.data or {}
|
||||
if not data.problems or #data.problems == 0 then
|
||||
logger.log(
|
||||
('No problems returned for %s contest %s.'):format(platform, contest_id),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
if type(callback) == 'function' then
|
||||
callback(data)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.scrape_contest_list(platform, callback)
|
||||
cache.load()
|
||||
|
||||
local cached = cache.get_contest_list(platform)
|
||||
if cached then
|
||||
callback({ success = true, contests = cached })
|
||||
return
|
||||
function M.scrape_contest_list(platform)
|
||||
local result = run_scraper(platform, 'contests', {}, { sync = true })
|
||||
if not result.success or not result.data.contests then
|
||||
logger.log(
|
||||
('Could not scrape contests list for platform %s: %s'):format(platform, result.msg),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return {}
|
||||
end
|
||||
|
||||
run_scraper(platform, 'contests', {}, function(result)
|
||||
if result.success and result.contests then
|
||||
cache.set_contest_list(platform, result.contests)
|
||||
end
|
||||
callback(result)
|
||||
end)
|
||||
return result.data.contests
|
||||
end
|
||||
|
||||
function M.scrape_problem_tests(platform, contest_id, problem_id, callback)
|
||||
run_scraper(platform, 'tests', { contest_id, problem_id }, function(result)
|
||||
if result.success and result.tests then
|
||||
vim.schedule(function()
|
||||
local mkdir_ok = pcall(vim.fn.mkdir, 'io', 'p')
|
||||
if mkdir_ok then
|
||||
local config = require('cp.config')
|
||||
local base_name = config.default_filename(contest_id, problem_id)
|
||||
run_scraper(platform, 'tests', { contest_id, problem_id }, {
|
||||
on_exit = function(result)
|
||||
if not result.success or not result.data.tests then
|
||||
logger.log(
|
||||
'Failed to load tests: ' .. (result.msg or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
|
||||
for i, test_case in ipairs(result.tests) do
|
||||
local input_file = 'io/' .. base_name .. '.' .. i .. '.cpin'
|
||||
local expected_file = 'io/' .. base_name .. '.' .. i .. '.cpout'
|
||||
|
||||
local input_content = test_case.input:gsub('\r', '')
|
||||
local expected_content = test_case.expected:gsub('\r', '')
|
||||
|
||||
pcall(vim.fn.writefile, vim.split(input_content, '\n', true), input_file)
|
||||
pcall(vim.fn.writefile, vim.split(expected_content, '\n', true), expected_file)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local cached_tests = {}
|
||||
for i, test_case in ipairs(result.tests) do
|
||||
table.insert(cached_tests, {
|
||||
index = i,
|
||||
input = test_case.input,
|
||||
expected = test_case.expected,
|
||||
})
|
||||
return {}
|
||||
end
|
||||
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
problem_id,
|
||||
cached_tests,
|
||||
result.timeout_ms,
|
||||
result.memory_mb
|
||||
)
|
||||
end
|
||||
vim.schedule(function()
|
||||
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
||||
local config = require('cp.config')
|
||||
local base_name = config.default_filename(contest_id, problem_id)
|
||||
|
||||
callback(result)
|
||||
end)
|
||||
for i, test_case in ipairs(result.data.tests) do
|
||||
local input_file = 'io/' .. base_name .. '.' .. i .. '.cpin'
|
||||
local expected_file = 'io/' .. base_name .. '.' .. i .. '.cpout'
|
||||
|
||||
local input_content = test_case.input:gsub('\r', '')
|
||||
local expected_content = test_case.expected:gsub('\r', '')
|
||||
|
||||
pcall(vim.fn.writefile, vim.split(input_content, '\n', { trimempty = true }), input_file)
|
||||
pcall(
|
||||
vim.fn.writefile,
|
||||
vim.split(expected_content, '\n', { trimempty = true }),
|
||||
expected_file
|
||||
)
|
||||
end
|
||||
if type(callback) == 'function' then
|
||||
callback(result.data)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
289
lua/cp/setup.lua
289
lua/cp/setup.lua
|
|
@ -28,196 +28,154 @@ function M.set_platform(platform)
|
|||
return true
|
||||
end
|
||||
|
||||
function M.setup_contest(platform, contest_id, problem_id, language)
|
||||
if not state.get_platform() then
|
||||
logger.log('No platform configured. Use :CP <platform> <contest> [...] first.')
|
||||
|
||||
local function backfill_missing_tests(platform, contest_id, problems)
|
||||
cache.load()
|
||||
local missing = {}
|
||||
for _, prob in ipairs(problems) do
|
||||
if not cache.get_test_cases(platform, contest_id, prob.id) then
|
||||
table.insert(missing, prob.id)
|
||||
end
|
||||
end
|
||||
if #missing == 0 then
|
||||
logger.log(('All problems already cached for %s contest %s.'):format(platform, contest_id))
|
||||
return
|
||||
end
|
||||
|
||||
local config = config_module.get_config()
|
||||
|
||||
if not vim.tbl_contains(config.scrapers, platform) then
|
||||
logger.log('scraping disabled for ' .. platform, vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
logger.log('fetching contests problems...', vim.log.levels.INFO, true)
|
||||
|
||||
scraper.scrape_contest_metadata(platform, contest_id, function(result)
|
||||
if not result.success then
|
||||
logger.log(
|
||||
'failed to load contest metadata: ' .. (result.error or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local problems = result.problems
|
||||
if not problems or #problems == 0 then
|
||||
logger.log('no problems found in contest', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
logger.log(('found %d problems'):format(#problems))
|
||||
|
||||
state.set_contest_id(contest_id)
|
||||
local target_problem = problem_id or problems[1].id
|
||||
|
||||
if problem_id then
|
||||
local problem_exists = false
|
||||
for _, prob in ipairs(problems) do
|
||||
if prob.id == problem_id then
|
||||
problem_exists = true
|
||||
break
|
||||
for _, pid in ipairs(missing) do
|
||||
local captured = pid
|
||||
scraper.scrape_problem_tests(platform, contest_id, captured, function(result)
|
||||
local cached_tests = {}
|
||||
if result.tests then
|
||||
for i, t in ipairs(result.tests) do
|
||||
cached_tests[i] = { index = i, input = t.input, expected = t.expected }
|
||||
end
|
||||
end
|
||||
if not problem_exists then
|
||||
logger.log(
|
||||
('invalid problem %s for contest %s'):format(problem_id, contest_id),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
M.setup_problem(contest_id, target_problem, language)
|
||||
|
||||
M.scrape_remaining_problems(platform, contest_id, problems)
|
||||
end)
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
captured,
|
||||
cached_tests,
|
||||
result.timeout_ms,
|
||||
result.memory_mb
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup_problem(contest_id, problem_id, language)
|
||||
if not state.get_platform() then
|
||||
logger.log('no platform set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
|
||||
function M.setup_contest(platform, contest_id, language, problem_id)
|
||||
if not platform then
|
||||
logger.log('No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug} first.')
|
||||
return
|
||||
end
|
||||
|
||||
local config = config_module.get_config()
|
||||
local platform = state.get_platform() or ''
|
||||
|
||||
logger.log(('setting up problem %s%s...'):format(contest_id, problem_id or ''))
|
||||
if not vim.tbl_contains(config.scrapers, platform) then
|
||||
logger.log(('Scraping disabled for %s.'):format(platform), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
state.set_contest_id(contest_id)
|
||||
state.set_problem_id(problem_id)
|
||||
-- TODO: why comment this out
|
||||
-- state.set_active_panel('run')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
|
||||
vim.schedule(function()
|
||||
local ok, err = pcall(function()
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
|
||||
local source_file = state.get_source_file(language)
|
||||
if not source_file then
|
||||
return
|
||||
end
|
||||
vim.cmd.e(source_file)
|
||||
local source_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
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 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(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 })
|
||||
vim.cmd.startinsert({ bang = true })
|
||||
|
||||
vim.schedule(function()
|
||||
if luasnip.expandable() then
|
||||
luasnip.expand()
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
end
|
||||
vim.cmd.stopinsert()
|
||||
end)
|
||||
else
|
||||
vim.api.nvim_input(('i%s<c-space><esc>'):format(platform))
|
||||
end
|
||||
end
|
||||
|
||||
if config.hooks and config.hooks.setup_code then
|
||||
config.hooks.setup_code(state)
|
||||
end
|
||||
|
||||
cache.set_file_state(vim.fn.expand('%:p'), platform, contest_id, problem_id, language)
|
||||
|
||||
logger.log(('ready - problem %s'):format(state.get_base_name()))
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
logger.log(('setup error: %s'):format(err), vim.log.levels.ERROR)
|
||||
end
|
||||
end)
|
||||
|
||||
local cached_tests = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
if cached_tests then
|
||||
state.set_test_cases(cached_tests)
|
||||
logger.log(('using cached test cases (%d)'):format(#cached_tests))
|
||||
elseif vim.tbl_contains(config.scrapers, platform) then
|
||||
logger.log('loading test cases...')
|
||||
|
||||
scraper.scrape_problem_tests(platform, contest_id, problem_id, function(result)
|
||||
if result.success then
|
||||
logger.log(('loaded %d test cases for %s'):format(#(result.tests or {}), problem_id))
|
||||
if state.get_problem_id() == problem_id then
|
||||
state.set_test_cases(result.tests)
|
||||
end
|
||||
else
|
||||
logger.log(
|
||||
'failed to load tests: ' .. (result.error or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
if state.get_problem_id() == problem_id then
|
||||
state.set_test_cases({})
|
||||
end
|
||||
if not contest_data or not contest_data.problems then
|
||||
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
|
||||
scraper.scrape_contest_metadata(platform, contest_id, function(result)
|
||||
local problems = result.problems or {}
|
||||
cache.set_contest_data(platform, contest_id, problems)
|
||||
logger.log(('Found %d problems for %s contest %s.'):format(#problems, platform, contest_id))
|
||||
local pid = problem_id or (problems[1] and problems[1].id)
|
||||
if pid then
|
||||
M.setup_problem(pid, language)
|
||||
end
|
||||
backfill_missing_tests(platform, contest_id, problems)
|
||||
end)
|
||||
else
|
||||
logger.log(('scraping disabled for %s'):format(platform))
|
||||
state.set_test_cases({})
|
||||
local problems = contest_data.problems
|
||||
local pid = problem_id or (problems[1] and problems[1].id)
|
||||
if pid then
|
||||
M.setup_problem(pid, language)
|
||||
end
|
||||
backfill_missing_tests(platform, contest_id, problems)
|
||||
end
|
||||
end
|
||||
|
||||
function M.scrape_remaining_problems(platform, contest_id, problems)
|
||||
cache.load()
|
||||
local missing_problems = {}
|
||||
|
||||
for _, prob in ipairs(problems) do
|
||||
local cached_tests = cache.get_test_cases(platform, contest_id, prob.id)
|
||||
if not cached_tests then
|
||||
table.insert(missing_problems, prob)
|
||||
end
|
||||
end
|
||||
|
||||
if #missing_problems == 0 then
|
||||
logger.log('all problems already cached')
|
||||
---@param problem_id string
|
||||
---@param language? string
|
||||
function M.setup_problem(problem_id, language)
|
||||
local platform = state.get_platform()
|
||||
if not platform then
|
||||
logger.log(
|
||||
'No platform set. run :CP <platform> <contest> [--{lang=<lang>,debug}]',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
logger.log(('caching %d remaining problems...'):format(#missing_problems))
|
||||
state.set_problem_id(problem_id)
|
||||
|
||||
for _, prob in ipairs(missing_problems) do
|
||||
scraper.scrape_problem_tests(platform, contest_id, prob.id, function(result)
|
||||
if result.success then
|
||||
logger.log(('background: scraped problem %s'):format(prob.id))
|
||||
local config = config_module.get_config()
|
||||
|
||||
vim.schedule(function()
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
|
||||
local source_file = state.get_source_file(language)
|
||||
if not source_file then
|
||||
return
|
||||
end
|
||||
vim.cmd.e(source_file)
|
||||
local source_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
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 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(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 })
|
||||
vim.cmd.startinsert({ bang = true })
|
||||
|
||||
vim.schedule(function()
|
||||
if luasnip.expandable() then
|
||||
luasnip.expand()
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
end
|
||||
vim.cmd.stopinsert()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if config.hooks and config.hooks.setup_code then
|
||||
config.hooks.setup_code(state)
|
||||
end
|
||||
|
||||
cache.set_file_state(
|
||||
vim.fn.expand('%:p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id(),
|
||||
language
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.navigate_problem(direction, language)
|
||||
if direction == 0 then
|
||||
return
|
||||
end
|
||||
direction = direction > 0 and 1 or -1
|
||||
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local current_problem_id = state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not current_problem_id then
|
||||
logger.log(
|
||||
'No platform configured. Use :CP <platform> <contest> [...] first.',
|
||||
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -226,32 +184,35 @@ function M.navigate_problem(direction, language)
|
|||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if not contest_data or not contest_data.problems then
|
||||
logger.log('no contest data available', vim.log.levels.ERROR)
|
||||
logger.log(
|
||||
('No data available for %s contest %s.'):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local problems = contest_data.problems
|
||||
local current_index = nil
|
||||
local current_index
|
||||
for i, prob in ipairs(problems) do
|
||||
if prob.id == current_problem_id then
|
||||
current_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not current_index then
|
||||
logger.log('current problem not found in contest', vim.log.levels.ERROR)
|
||||
M.setup_contest(platform, contest_id, language, problems[1].id)
|
||||
return
|
||||
end
|
||||
|
||||
local new_index = current_index + direction
|
||||
if new_index < 1 or new_index > #problems then
|
||||
logger.log('no more problems in that direction', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local new_problem = problems[new_index]
|
||||
M.setup_problem(contest_id, new_problem.id, language)
|
||||
M.setup_contest(platform, contest_id, language, problems[new_index].id)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ local logger = require('cp.log')
|
|||
function M.setup(config)
|
||||
local ok, ls = pcall(require, 'luasnip')
|
||||
if not ok then
|
||||
logger.log('LuaSnip not available - snippets disabled', vim.log.levels.INFO)
|
||||
logger.log('LuaSnip not available - snippets are disabled.', vim.log.levels.INFO, true)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
---@field set_problem_id fun(problem_id: string)
|
||||
---@field get_active_panel fun(): string?
|
||||
---@field set_active_panel fun(): string?
|
||||
---@field get_test_cases fun(): table[]?
|
||||
---@field set_test_cases fun(test_cases: table[])
|
||||
---@field get_saved_session fun(): table?
|
||||
---@field set_saved_session fun(session: table)
|
||||
---@field get_context fun(): {platform: string?, contest_id: string?, problem_id: string?}
|
||||
|
|
@ -56,14 +54,6 @@ function M.set_problem_id(problem_id)
|
|||
state.problem_id = problem_id
|
||||
end
|
||||
|
||||
function M.get_test_cases()
|
||||
return state.test_cases
|
||||
end
|
||||
|
||||
function M.set_test_cases(test_cases)
|
||||
state.test_cases = test_cases
|
||||
end
|
||||
|
||||
function M.get_saved_session()
|
||||
return state.saved_session
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ function M.toggle_interactive()
|
|||
state.saved_interactive_session = nil
|
||||
end
|
||||
state.set_active_panel(nil)
|
||||
logger.log('interactive closed')
|
||||
logger.log('Interactive panel closed.')
|
||||
return
|
||||
end
|
||||
|
||||
if state.get_active_panel() then
|
||||
logger.log('another panel is already active', vim.log.levels.ERROR)
|
||||
logger.log('Another panel is already active.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ function M.toggle_interactive()
|
|||
|
||||
if not platform then
|
||||
logger.log(
|
||||
'No platform configured. Use :CP <platform> <contest> [...] first.',
|
||||
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -44,7 +44,7 @@ function M.toggle_interactive()
|
|||
|
||||
if not contest_id then
|
||||
logger.log(
|
||||
('No contest %s configured for platform %s. Use :CP <platform> <contest> <problem> to set up first.'):format(
|
||||
('No contest %s configured for platform %s. Use :CP <platform> <contest> [--{lang=<lang>,debug}] to set up first.'):format(
|
||||
contest_id,
|
||||
platform
|
||||
),
|
||||
|
|
@ -63,10 +63,7 @@ function M.toggle_interactive()
|
|||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if contest_data and not contest_data.interactive then
|
||||
logger.log(
|
||||
'This is NOT an interactive problem. Use :CP run instead - aborting.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
logger.log('This is NOT an interactive problem. Use :CP run instead.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -95,7 +92,7 @@ function M.toggle_interactive()
|
|||
|
||||
vim.fn.chansend(vim.b.terminal_job_id, binary .. '\n')
|
||||
|
||||
vim.keymap.set('t', config.run_panel.close_key, function()
|
||||
vim.keymap.set('t', '<c-q>', function()
|
||||
M.toggle_interactive()
|
||||
end, { buffer = term_buf, silent = true })
|
||||
|
||||
|
|
@ -139,7 +136,7 @@ function M.toggle_run_panel(is_debug)
|
|||
|
||||
if not contest_id then
|
||||
logger.log(
|
||||
('No contest %s configured for platform %s. Use :CP <platform> <contest> <problem> to set up first.'):format(
|
||||
('No contest %s configured for platform %s. Use :CP <platform> <contest> [--{lang=<lang>,debug}] to set up first.'):format(
|
||||
contest_id,
|
||||
platform
|
||||
),
|
||||
|
|
@ -158,15 +155,12 @@ function M.toggle_run_panel(is_debug)
|
|||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if contest_data and contest_data.interactive then
|
||||
logger.log(
|
||||
'This is an interactive problem. Use :CP interact instead - aborting.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
logger.log('This is an interactive problem. Use :CP interact instead.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
logger.log(
|
||||
('run panel: platform=%s, contest=%s, problem=%s'):format(
|
||||
('Run panel: platform=%s, contest=%s, problem=%s'):format(
|
||||
tostring(platform),
|
||||
tostring(contest_id),
|
||||
tostring(problem_id)
|
||||
|
|
@ -235,18 +229,18 @@ function M.toggle_run_panel(is_debug)
|
|||
|
||||
local function navigate_test_case(delta)
|
||||
local test_state = run.get_run_panel_state()
|
||||
if #test_state.test_cases == 0 then
|
||||
if vim.tbl_isempty(test_state.test_cases) then
|
||||
return
|
||||
end
|
||||
test_state.current_index = (test_state.current_index + delta) % #test_state.test_cases
|
||||
test_state.current_index = (test_state.current_index + delta - 1) % #test_state.test_cases + 1
|
||||
refresh_run_panel()
|
||||
end
|
||||
|
||||
setup_keybindings_for_buffer = function(buf)
|
||||
vim.keymap.set('n', config.run_panel.close_key, function()
|
||||
vim.keymap.set('n', 'q', function()
|
||||
M.toggle_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.toggle_diff_key, function()
|
||||
vim.keymap.set('n', 't', function()
|
||||
local modes = { 'none', 'git', 'vim' }
|
||||
local current_idx = nil
|
||||
for i, mode in ipairs(modes) do
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ end, {
|
|||
if args[2] == 'cache' then
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, { 'clear' })
|
||||
end, { 'clear', 'read' })
|
||||
end
|
||||
elseif num_args == 4 then
|
||||
if args[2] == 'cache' and args[3] == 'clear' then
|
||||
|
|
|
|||
|
|
@ -1,243 +0,0 @@
|
|||
describe('ansi parser', function()
|
||||
local ansi = require('cp.ui.ansi')
|
||||
|
||||
describe('bytes_to_string', function()
|
||||
it('returns string as-is', function()
|
||||
local input = 'hello world'
|
||||
assert.equals('hello world', ansi.bytes_to_string(input))
|
||||
end)
|
||||
|
||||
it('converts byte array to string', function()
|
||||
local input = { 104, 101, 108, 108, 111 }
|
||||
assert.equals('hello', ansi.bytes_to_string(input))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse_ansi_text', function()
|
||||
it('strips ansi codes from simple text', function()
|
||||
local input = 'Hello \027[31mworld\027[0m!'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello world!', table.concat(result.lines, '\n'))
|
||||
end)
|
||||
|
||||
it('handles text without ansi codes', function()
|
||||
local input = 'Plain text'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Plain text', table.concat(result.lines, '\n'))
|
||||
assert.equals(0, #result.highlights)
|
||||
end)
|
||||
|
||||
it('creates correct highlight for simple colored text', function()
|
||||
local input = 'Hello \027[31mworld\027[0m!'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals(0, highlight.line)
|
||||
assert.equals(6, highlight.col_start)
|
||||
assert.equals(11, highlight.col_end)
|
||||
assert.equals('CpAnsiRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles bold text', function()
|
||||
local input = 'Hello \027[1mbold\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBold', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles italic text', function()
|
||||
local input = 'Hello \027[3mitalic\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello italic world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiItalic', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles bold + color combination', function()
|
||||
local input = 'Hello \027[1;31mbold red\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold red world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', highlight.highlight_group)
|
||||
assert.equals(6, highlight.col_start)
|
||||
assert.equals(14, highlight.col_end)
|
||||
end)
|
||||
|
||||
it('handles italic + color combination', function()
|
||||
local input = 'Hello \027[3;32mitalic green\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello italic green world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiItalicGreen', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles bold + italic + color combination', function()
|
||||
local input = 'Hello \027[1;3;33mbold italic yellow\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold italic yellow world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldItalicYellow', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles sequential attribute setting', function()
|
||||
local input = 'Hello \027[1m\027[31mbold red\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold red world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles selective attribute reset', function()
|
||||
local input = 'Hello \027[1;31mbold red\027[22mno longer bold\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold redno longer bold world', table.concat(result.lines, '\n'))
|
||||
assert.equals(2, #result.highlights)
|
||||
|
||||
local bold_red = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', bold_red.highlight_group)
|
||||
assert.equals(6, bold_red.col_start)
|
||||
assert.equals(14, bold_red.col_end)
|
||||
|
||||
local just_red = result.highlights[2]
|
||||
assert.equals('CpAnsiRed', just_red.highlight_group)
|
||||
assert.equals(14, just_red.col_start)
|
||||
assert.equals(28, just_red.col_end)
|
||||
end)
|
||||
|
||||
it('handles bright colors', function()
|
||||
local input = 'Hello \027[91mbright red\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBrightRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles compiler-like output with complex formatting', function()
|
||||
local input =
|
||||
"error.cpp:10:5: \027[1m\027[31merror:\027[0m\027[1m 'undefined' was not declared\027[0m"
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
local clean_text = table.concat(result.lines, '\n')
|
||||
assert.equals("error.cpp:10:5: error: 'undefined' was not declared", clean_text)
|
||||
assert.equals(2, #result.highlights)
|
||||
|
||||
local error_highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', error_highlight.highlight_group)
|
||||
assert.equals(16, error_highlight.col_start)
|
||||
assert.equals(22, error_highlight.col_end)
|
||||
|
||||
local message_highlight = result.highlights[2]
|
||||
assert.equals('CpAnsiBold', message_highlight.highlight_group)
|
||||
assert.equals(22, message_highlight.col_start)
|
||||
assert.equals(51, message_highlight.col_end)
|
||||
end)
|
||||
|
||||
it('handles multiline with persistent state', function()
|
||||
local input = '\027[1;31mline1\nline2\nline3\027[0m'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('line1\nline2\nline3', table.concat(result.lines, '\n'))
|
||||
assert.equals(3, #result.highlights)
|
||||
|
||||
for i, highlight in ipairs(result.highlights) do
|
||||
assert.equals('CpAnsiBoldRed', highlight.highlight_group)
|
||||
assert.equals(i - 1, highlight.line)
|
||||
assert.equals(0, highlight.col_start)
|
||||
assert.equals(5, highlight.col_end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('update_ansi_state', function()
|
||||
it('resets all state on reset code', function()
|
||||
local state = { bold = true, italic = true, foreground = 'Red' }
|
||||
ansi.update_ansi_state(state, '0')
|
||||
|
||||
assert.is_false(state.bold)
|
||||
assert.is_false(state.italic)
|
||||
assert.is_nil(state.foreground)
|
||||
end)
|
||||
|
||||
it('sets individual attributes', function()
|
||||
local state = { bold = false, italic = false, foreground = nil }
|
||||
|
||||
ansi.update_ansi_state(state, '1')
|
||||
assert.is_true(state.bold)
|
||||
|
||||
ansi.update_ansi_state(state, '3')
|
||||
assert.is_true(state.italic)
|
||||
|
||||
ansi.update_ansi_state(state, '31')
|
||||
assert.equals('Red', state.foreground)
|
||||
end)
|
||||
|
||||
it('handles compound codes', function()
|
||||
local state = { bold = false, italic = false, foreground = nil }
|
||||
ansi.update_ansi_state(state, '1;3;31')
|
||||
|
||||
assert.is_true(state.bold)
|
||||
assert.is_true(state.italic)
|
||||
assert.equals('Red', state.foreground)
|
||||
end)
|
||||
|
||||
it('handles selective resets', function()
|
||||
local state = { bold = true, italic = true, foreground = 'Red' }
|
||||
|
||||
ansi.update_ansi_state(state, '22')
|
||||
assert.is_false(state.bold)
|
||||
assert.is_true(state.italic)
|
||||
assert.equals('Red', state.foreground)
|
||||
|
||||
ansi.update_ansi_state(state, '39')
|
||||
assert.is_false(state.bold)
|
||||
assert.is_true(state.italic)
|
||||
assert.is_nil(state.foreground)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('setup_highlight_groups', function()
|
||||
it('creates highlight groups with fallback colors when terminal colors are nil', function()
|
||||
local original_colors = {}
|
||||
for i = 0, 15 do
|
||||
original_colors[i] = vim.g['terminal_color_' .. i]
|
||||
vim.g['terminal_color_' .. i] = nil
|
||||
end
|
||||
|
||||
ansi.setup_highlight_groups()
|
||||
|
||||
local highlight = vim.api.nvim_get_hl(0, { name = 'CpAnsiRed' })
|
||||
assert.is_nil(highlight.fg)
|
||||
|
||||
for i = 0, 15 do
|
||||
vim.g['terminal_color_' .. i] = original_colors[i]
|
||||
end
|
||||
end)
|
||||
|
||||
it('creates highlight groups with proper colors when terminal colors are set', function()
|
||||
vim.g.terminal_color_1 = '#ff0000'
|
||||
|
||||
ansi.setup_highlight_groups()
|
||||
|
||||
local highlight = vim.api.nvim_get_hl(0, { name = 'CpAnsiRed' })
|
||||
assert.equals(0xff0000, highlight.fg)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
describe('cp.cache', function()
|
||||
local cache
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
|
||||
local mock_file_content = '{}'
|
||||
vim.fn.filereadable = function()
|
||||
return 1
|
||||
end
|
||||
vim.fn.readfile = function()
|
||||
return { mock_file_content }
|
||||
end
|
||||
vim.fn.writefile = function(lines)
|
||||
mock_file_content = table.concat(lines, '\n')
|
||||
end
|
||||
vim.fn.mkdir = function() end
|
||||
|
||||
cache = require('cp.cache')
|
||||
cache.load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
cache.clear_contest_data('atcoder', 'test_contest')
|
||||
cache.clear_contest_data('codeforces', 'test_contest')
|
||||
cache.clear_contest_data('cses', 'test_contest')
|
||||
end)
|
||||
|
||||
describe('load and save', function()
|
||||
it('loads without error when cache file exists', function()
|
||||
assert.has_no_errors(function()
|
||||
cache.load()
|
||||
end)
|
||||
end)
|
||||
|
||||
it('saves and persists data', function()
|
||||
local problems = { { id = 'A', name = 'Problem A' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cache.set_contest_data('atcoder', 'test_contest', problems)
|
||||
end)
|
||||
|
||||
local result = cache.get_contest_data('atcoder', 'test_contest')
|
||||
assert.is_not_nil(result)
|
||||
assert.equals('A', result.problems[1].id)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('contest data', function()
|
||||
it('stores and retrieves contest data', function()
|
||||
local problems = {
|
||||
{ id = 'A', name = 'First Problem' },
|
||||
{ id = 'B', name = 'Second Problem' },
|
||||
}
|
||||
|
||||
cache.set_contest_data('codeforces', 'test_contest', problems)
|
||||
local result = cache.get_contest_data('codeforces', 'test_contest')
|
||||
|
||||
assert.is_not_nil(result)
|
||||
assert.equals(2, #result.problems)
|
||||
assert.equals('A', result.problems[1].id)
|
||||
assert.equals('Second Problem', result.problems[2].name)
|
||||
end)
|
||||
|
||||
it('returns nil for missing contest', function()
|
||||
local result = cache.get_contest_data('atcoder', 'nonexistent_contest')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it('clears contest data', function()
|
||||
local problems = { { id = 'A' } }
|
||||
cache.set_contest_data('atcoder', 'test_contest', problems)
|
||||
|
||||
cache.clear_contest_data('atcoder', 'test_contest')
|
||||
local result = cache.get_contest_data('atcoder', 'test_contest')
|
||||
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it('handles cses expiry correctly', function()
|
||||
local problems = { { id = 'A' } }
|
||||
cache.set_contest_data('cses', 'test_contest', problems)
|
||||
|
||||
local result = cache.get_contest_data('cses', 'test_contest')
|
||||
assert.is_not_nil(result)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('test cases', function()
|
||||
it('stores and retrieves test cases', function()
|
||||
local test_cases = {
|
||||
{ index = 1, input = '1 2', expected = '3' },
|
||||
{ index = 2, input = '4 5', expected = '9' },
|
||||
}
|
||||
|
||||
cache.set_test_cases('atcoder', 'test_contest', 'A', test_cases)
|
||||
local result = cache.get_test_cases('atcoder', 'test_contest', 'A')
|
||||
|
||||
assert.is_not_nil(result)
|
||||
assert.equals(2, #result)
|
||||
assert.equals('1 2', result[1].input)
|
||||
assert.equals('9', result[2].expected)
|
||||
end)
|
||||
|
||||
it('handles contest-level test cases', function()
|
||||
local test_cases = { { input = 'test', expected = 'output' } }
|
||||
|
||||
cache.set_test_cases('cses', 'test_contest', nil, test_cases)
|
||||
local result = cache.get_test_cases('cses', 'test_contest', nil)
|
||||
|
||||
assert.is_not_nil(result)
|
||||
assert.equals(1, #result)
|
||||
assert.equals('test', result[1].input)
|
||||
end)
|
||||
|
||||
it('returns nil for missing test cases', function()
|
||||
local result = cache.get_test_cases('atcoder', 'nonexistent', 'A')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('file state', function()
|
||||
it('stores and retrieves file state', function()
|
||||
local file_path = '/tmp/test.cpp'
|
||||
|
||||
cache.set_file_state(file_path, 'atcoder', 'abc123', 'a', 'cpp')
|
||||
local result = cache.get_file_state(file_path)
|
||||
|
||||
assert.is_not_nil(result)
|
||||
assert.equals('atcoder', result.platform)
|
||||
assert.equals('abc123', result.contest_id)
|
||||
assert.equals('a', result.problem_id)
|
||||
assert.equals('cpp', result.language)
|
||||
end)
|
||||
|
||||
it('handles cses file state without problem_id', function()
|
||||
local file_path = '/tmp/cses.py'
|
||||
|
||||
cache.set_file_state(file_path, 'cses', '1068', nil, 'python')
|
||||
local result = cache.get_file_state(file_path)
|
||||
|
||||
assert.is_not_nil(result)
|
||||
assert.equals('cses', result.platform)
|
||||
assert.equals('1068', result.contest_id)
|
||||
assert.is_nil(result.problem_id)
|
||||
assert.equals('python', result.language)
|
||||
end)
|
||||
|
||||
it('returns nil for missing file state', function()
|
||||
local result = cache.get_file_state('/nonexistent/file.cpp')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it('overwrites existing file state', function()
|
||||
local file_path = '/tmp/overwrite.cpp'
|
||||
|
||||
cache.set_file_state(file_path, 'atcoder', 'abc123', 'a', 'cpp')
|
||||
cache.set_file_state(file_path, 'codeforces', '1934', 'b', 'python')
|
||||
|
||||
local result = cache.get_file_state(file_path)
|
||||
|
||||
assert.is_not_nil(result)
|
||||
assert.equals('codeforces', result.platform)
|
||||
assert.equals('1934', result.contest_id)
|
||||
assert.equals('b', result.problem_id)
|
||||
assert.equals('python', result.language)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('cache management', function()
|
||||
it('clears all cache data', function()
|
||||
cache.set_contest_data('atcoder', 'test_contest', { { id = 'A' } })
|
||||
cache.set_contest_data('codeforces', 'test_contest', { { id = 'B' } })
|
||||
cache.set_file_state('/tmp/test.cpp', 'atcoder', 'abc123', 'a', 'cpp')
|
||||
|
||||
cache.clear_all()
|
||||
|
||||
assert.is_nil(cache.get_contest_data('atcoder', 'test_contest'))
|
||||
assert.is_nil(cache.get_contest_data('codeforces', 'test_contest'))
|
||||
assert.is_nil(cache.get_file_state('/tmp/test.cpp'))
|
||||
end)
|
||||
|
||||
it('clears cache for specific platform', function()
|
||||
cache.set_contest_data('atcoder', 'test_contest', { { id = 'A' } })
|
||||
cache.set_contest_data('codeforces', 'test_contest', { { id = 'B' } })
|
||||
cache.set_contest_list('atcoder', { { id = '123', name = 'Test' } })
|
||||
cache.set_contest_list('codeforces', { { id = '456', name = 'Test' } })
|
||||
|
||||
cache.clear_platform('atcoder')
|
||||
|
||||
assert.is_nil(cache.get_contest_data('atcoder', 'test_contest'))
|
||||
assert.is_nil(cache.get_contest_list('atcoder'))
|
||||
assert.is_not_nil(cache.get_contest_data('codeforces', 'test_contest'))
|
||||
assert.is_not_nil(cache.get_contest_list('codeforces'))
|
||||
end)
|
||||
|
||||
it('handles clear platform for non-existent platform', function()
|
||||
assert.has_no_errors(function()
|
||||
cache.clear_platform('nonexistent')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,730 +0,0 @@
|
|||
describe('cp command parsing', function()
|
||||
local cp
|
||||
local logged_messages
|
||||
|
||||
before_each(function()
|
||||
logged_messages = {}
|
||||
local mock_logger = {
|
||||
log = function(msg, level)
|
||||
table.insert(logged_messages, { msg = msg, level = level })
|
||||
end,
|
||||
set_config = function() end,
|
||||
}
|
||||
package.loaded['cp.log'] = mock_logger
|
||||
|
||||
local mock_setup = {
|
||||
set_platform = function()
|
||||
return true
|
||||
end,
|
||||
setup_contest = function() end,
|
||||
navigate_problem = function() end,
|
||||
setup_problem = function() end,
|
||||
scrape_remaining_problems = function() end,
|
||||
}
|
||||
package.loaded['cp.setup'] = mock_setup
|
||||
|
||||
local mock_state = {
|
||||
get_platform = function()
|
||||
return 'atcoder'
|
||||
end,
|
||||
get_contest_id = function()
|
||||
return 'abc123'
|
||||
end,
|
||||
get_problem_id = function()
|
||||
return 'a'
|
||||
end,
|
||||
set_platform = function() end,
|
||||
set_contest_id = function() end,
|
||||
set_problem_id = function() end,
|
||||
}
|
||||
package.loaded['cp.state'] = mock_state
|
||||
|
||||
local mock_ui_panel = {
|
||||
toggle_run_panel = function() end,
|
||||
toggle_interactive = function() end,
|
||||
}
|
||||
package.loaded['cp.ui.panel'] = mock_ui_panel
|
||||
|
||||
local mock_cache = {
|
||||
load = function() end,
|
||||
get_contest_data = function()
|
||||
return {
|
||||
problems = {
|
||||
{ id = 'a', name = 'Problem A' },
|
||||
{ id = 'b', name = 'Problem B' },
|
||||
},
|
||||
}
|
||||
end,
|
||||
}
|
||||
package.loaded['cp.cache'] = mock_cache
|
||||
|
||||
local mock_restore = {
|
||||
restore_from_current_file = function()
|
||||
logged_messages[#logged_messages + 1] =
|
||||
{ msg = 'No file is currently open', level = vim.log.levels.ERROR }
|
||||
end,
|
||||
}
|
||||
package.loaded['cp.restore'] = mock_restore
|
||||
|
||||
local mock_picker = {
|
||||
handle_pick_action = function() end,
|
||||
}
|
||||
package.loaded['cp.commands.picker'] = mock_picker
|
||||
|
||||
local mock_cache_commands = {
|
||||
handle_cache_command = function(cmd)
|
||||
if cmd.subcommand == 'clear' then
|
||||
if cmd.platform then
|
||||
local constants = require('cp.constants')
|
||||
if vim.tbl_contains(constants.PLATFORMS, cmd.platform) then
|
||||
logged_messages[#logged_messages + 1] = { msg = 'cleared cache for ' .. cmd.platform }
|
||||
else
|
||||
logged_messages[#logged_messages + 1] =
|
||||
{ msg = 'unknown platform: ' .. cmd.platform, level = vim.log.levels.ERROR }
|
||||
end
|
||||
else
|
||||
logged_messages[#logged_messages + 1] = { msg = 'cleared all cache' }
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
package.loaded['cp.commands.cache'] = mock_cache_commands
|
||||
|
||||
cp = require('cp')
|
||||
cp.setup({
|
||||
contests = {
|
||||
atcoder = {
|
||||
default_language = 'cpp',
|
||||
cpp = { extension = 'cpp' },
|
||||
},
|
||||
cses = {
|
||||
default_language = 'cpp',
|
||||
cpp = { extension = 'cpp' },
|
||||
},
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
package.loaded['cp.log'] = nil
|
||||
package.loaded['cp.setup'] = nil
|
||||
package.loaded['cp.state'] = nil
|
||||
package.loaded['cp.ui.panel'] = nil
|
||||
package.loaded['cp.cache'] = nil
|
||||
package.loaded['cp.restore'] = nil
|
||||
package.loaded['cp.commands.picker'] = nil
|
||||
package.loaded['cp.commands.cache'] = nil
|
||||
package.loaded['cp'] = nil
|
||||
package.loaded['cp.commands.init'] = nil
|
||||
end)
|
||||
|
||||
describe('empty arguments', function()
|
||||
it('attempts file state restoration for no arguments', function()
|
||||
local opts = { fargs = {} }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
assert.is_true(#logged_messages > 0)
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if
|
||||
log_entry.level == vim.log.levels.ERROR
|
||||
and log_entry.msg:match('No file is currently open')
|
||||
then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('action commands', function()
|
||||
it('handles test action without error', function()
|
||||
local opts = { fargs = { 'run' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles next action without error', function()
|
||||
local opts = { fargs = { 'next' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles prev action without error', function()
|
||||
local opts = { fargs = { 'prev' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('platform commands', function()
|
||||
it('handles platform-only command', function()
|
||||
local opts = { fargs = { 'atcoder' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles contest setup command', function()
|
||||
local opts = { fargs = { 'atcoder', 'abc123' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles cses problem command', function()
|
||||
local opts = { fargs = { 'cses', 'sorting_and_searching', '1234' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles full setup command', function()
|
||||
local opts = { fargs = { 'atcoder', 'abc123', 'a' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('logs error for too many arguments', function()
|
||||
local opts = { fargs = { 'atcoder', 'abc123', 'a', 'b', 'extra' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.level == vim.log.levels.ERROR then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('language flag parsing', function()
|
||||
it('logs error for --lang flag missing value', function()
|
||||
local opts = { fargs = { 'run', '--lang' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if
|
||||
log_entry.level == vim.log.levels.ERROR and log_entry.msg:match('--lang requires a value')
|
||||
then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
|
||||
it('handles language with equals format', function()
|
||||
local opts = { fargs = { 'atcoder', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles language with space format', function()
|
||||
local opts = { fargs = { 'atcoder', '--lang', 'cpp' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles contest with language flag', function()
|
||||
local opts = { fargs = { 'atcoder', 'abc123', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('debug flag parsing', function()
|
||||
it('handles debug flag without error', function()
|
||||
local opts = { fargs = { 'run', '--debug' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles combined language and debug flags', function()
|
||||
local opts = { fargs = { 'run', '--lang=cpp', '--debug' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('restore from file', function()
|
||||
it('returns restore_from_file type for empty args', function()
|
||||
local opts = { fargs = {} }
|
||||
local logged_error = false
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
for _, log in ipairs(logged_messages) do
|
||||
if log.level == vim.log.levels.ERROR and log.msg:match('No file is currently open') then
|
||||
logged_error = true
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(logged_error)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('invalid commands', function()
|
||||
it('logs error for invalid platform', function()
|
||||
local opts = { fargs = { 'invalid_platform' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.level == vim.log.levels.ERROR then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
|
||||
it('logs error for invalid action', function()
|
||||
local opts = { fargs = { 'invalid_action' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.level == vim.log.levels.ERROR then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('edge cases', function()
|
||||
it('handles empty string arguments', function()
|
||||
local opts = { fargs = { '' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.level == vim.log.levels.ERROR then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
|
||||
it('handles flag order variations', function()
|
||||
local opts = { fargs = { '--debug', 'run', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles multiple language flags', function()
|
||||
local opts = { fargs = { 'run', '--lang=cpp', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('command validation', function()
|
||||
it('validates platform names against constants', function()
|
||||
local constants = require('cp.constants')
|
||||
|
||||
for _, platform in ipairs(constants.PLATFORMS) do
|
||||
local opts = { fargs = { platform } }
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
it('validates action names against constants', function()
|
||||
local constants = require('cp.constants')
|
||||
|
||||
for _, action in ipairs(constants.ACTIONS) do
|
||||
local opts = { fargs = { action } }
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('cache commands', function()
|
||||
it('handles cache clear without platform', function()
|
||||
local opts = { fargs = { 'cache', 'clear' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
|
||||
local success_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.msg and log_entry.msg:match('cleared all cache') then
|
||||
success_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(success_logged)
|
||||
end)
|
||||
|
||||
it('handles cache clear with valid platform', function()
|
||||
local opts = { fargs = { 'cache', 'clear', 'atcoder' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
|
||||
local success_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.msg and log_entry.msg:match('cleared cache for atcoder') then
|
||||
success_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(success_logged)
|
||||
end)
|
||||
|
||||
it('logs error for cache clear with invalid platform', function()
|
||||
local opts = { fargs = { 'cache', 'clear', 'invalid_platform' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.level == vim.log.levels.ERROR and log_entry.msg:match('unknown platform') then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
|
||||
it('logs error for cache command without subcommand', function()
|
||||
local opts = { fargs = { 'cache' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if
|
||||
log_entry.level == vim.log.levels.ERROR
|
||||
and log_entry.msg:match('cache command requires subcommand')
|
||||
then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
|
||||
it('logs error for invalid cache subcommand', function()
|
||||
local opts = { fargs = { 'cache', 'invalid' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
local error_logged = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if
|
||||
log_entry.level == vim.log.levels.ERROR
|
||||
and log_entry.msg:match('unknown cache subcommand')
|
||||
then
|
||||
error_logged = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(error_logged)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('CP command completion', function()
|
||||
local complete_fn
|
||||
|
||||
before_each(function()
|
||||
package.loaded['cp'] = nil
|
||||
package.loaded['cp.cache'] = nil
|
||||
|
||||
complete_fn = function(ArgLead, CmdLine, _)
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
||||
local args = vim.split(vim.trim(CmdLine), '%s+')
|
||||
local num_args = #args
|
||||
if CmdLine:sub(-1) == ' ' then
|
||||
num_args = num_args + 1
|
||||
end
|
||||
|
||||
if num_args == 2 then
|
||||
local candidates = {}
|
||||
local state = require('cp.state')
|
||||
if state.get_platform() and state.get_contest_id() then
|
||||
vim.list_extend(candidates, actions)
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data =
|
||||
cache.get_contest_data(state.get_platform(), state.get_contest_id())
|
||||
if contest_data and contest_data.problems then
|
||||
for _, problem in ipairs(contest_data.problems) do
|
||||
table.insert(candidates, problem.id)
|
||||
end
|
||||
end
|
||||
else
|
||||
vim.list_extend(candidates, platforms)
|
||||
table.insert(candidates, 'cache')
|
||||
table.insert(candidates, 'pick')
|
||||
end
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
elseif num_args == 3 then
|
||||
if args[2] == 'cache' then
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, { 'clear' })
|
||||
end
|
||||
elseif num_args == 4 then
|
||||
if args[2] == 'cache' and args[3] == 'clear' then
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, platforms)
|
||||
elseif vim.tbl_contains(platforms, args[2]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(args[2], args[3])
|
||||
if contest_data and contest_data.problems then
|
||||
local candidates = {}
|
||||
for _, problem in ipairs(contest_data.problems) do
|
||||
table.insert(candidates, problem.id)
|
||||
end
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
end
|
||||
end
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
package.loaded['cp.state'] = {
|
||||
get_platform = function()
|
||||
return nil
|
||||
end,
|
||||
get_contest_id = function()
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
|
||||
package.loaded['cp.cache'] = {
|
||||
load = function() end,
|
||||
get_contest_data = function()
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
package.loaded['cp'] = nil
|
||||
package.loaded['cp.cache'] = nil
|
||||
end)
|
||||
|
||||
it('completes platforms and global actions without contest configuration', function()
|
||||
local result = complete_fn('', 'CP ', 3)
|
||||
|
||||
assert.is_table(result)
|
||||
|
||||
local has_atcoder = false
|
||||
local has_codeforces = false
|
||||
local has_cses = false
|
||||
local has_cache = false
|
||||
local has_pick = false
|
||||
local has_run = false
|
||||
local has_next = false
|
||||
local has_prev = false
|
||||
|
||||
for _, item in ipairs(result) do
|
||||
if item == 'atcoder' then
|
||||
has_atcoder = true
|
||||
end
|
||||
if item == 'codeforces' then
|
||||
has_codeforces = true
|
||||
end
|
||||
if item == 'cses' then
|
||||
has_cses = true
|
||||
end
|
||||
if item == 'cache' then
|
||||
has_cache = true
|
||||
end
|
||||
if item == 'pick' then
|
||||
has_pick = true
|
||||
end
|
||||
if item == 'run' then
|
||||
has_run = true
|
||||
end
|
||||
if item == 'next' then
|
||||
has_next = true
|
||||
end
|
||||
if item == 'prev' then
|
||||
has_prev = true
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(has_atcoder)
|
||||
assert.is_true(has_codeforces)
|
||||
assert.is_true(has_cses)
|
||||
assert.is_true(has_cache)
|
||||
assert.is_true(has_pick)
|
||||
assert.is_false(has_run)
|
||||
assert.is_false(has_next)
|
||||
assert.is_false(has_prev)
|
||||
end)
|
||||
|
||||
it('completes all actions and problems when contest context exists', function()
|
||||
package.loaded['cp.state'] = {
|
||||
get_platform = function()
|
||||
return 'atcoder'
|
||||
end,
|
||||
get_contest_id = function()
|
||||
return 'abc350'
|
||||
end,
|
||||
}
|
||||
package.loaded['cp.cache'] = {
|
||||
load = function() end,
|
||||
get_contest_data = function()
|
||||
return {
|
||||
problems = {
|
||||
{ id = 'a' },
|
||||
{ id = 'b' },
|
||||
{ id = 'c' },
|
||||
},
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
local result = complete_fn('', 'CP ', 3)
|
||||
|
||||
assert.is_table(result)
|
||||
|
||||
local items = {}
|
||||
for _, item in ipairs(result) do
|
||||
items[item] = true
|
||||
end
|
||||
|
||||
assert.is_true(items['run'])
|
||||
assert.is_true(items['next'])
|
||||
assert.is_true(items['prev'])
|
||||
assert.is_true(items['pick'])
|
||||
assert.is_true(items['cache'])
|
||||
|
||||
assert.is_true(items['a'])
|
||||
assert.is_true(items['b'])
|
||||
assert.is_true(items['c'])
|
||||
end)
|
||||
|
||||
it('completes cache subcommands', function()
|
||||
local result = complete_fn('c', 'CP cache c', 10)
|
||||
|
||||
assert.is_table(result)
|
||||
assert.equals(1, #result)
|
||||
assert.equals('clear', result[1])
|
||||
end)
|
||||
|
||||
it('completes cache subcommands with exact match', function()
|
||||
local result = complete_fn('clear', 'CP cache clear', 14)
|
||||
|
||||
assert.is_table(result)
|
||||
assert.equals(1, #result)
|
||||
assert.equals('clear', result[1])
|
||||
end)
|
||||
|
||||
it('completes platforms for cache clear', function()
|
||||
local result = complete_fn('a', 'CP cache clear a', 16)
|
||||
|
||||
assert.is_table(result)
|
||||
|
||||
local has_atcoder = false
|
||||
local has_cache = false
|
||||
|
||||
for _, item in ipairs(result) do
|
||||
if item == 'atcoder' then
|
||||
has_atcoder = true
|
||||
end
|
||||
if item == 'cache' then
|
||||
has_cache = true
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(has_atcoder)
|
||||
assert.is_false(has_cache)
|
||||
end)
|
||||
|
||||
it('filters completions based on current input', function()
|
||||
local result = complete_fn('at', 'CP at', 5)
|
||||
|
||||
assert.is_table(result)
|
||||
assert.equals(1, #result)
|
||||
assert.equals('atcoder', result[1])
|
||||
end)
|
||||
|
||||
it('returns empty array when no matches', function()
|
||||
local result = complete_fn('xyz', 'CP xyz', 6)
|
||||
|
||||
assert.is_table(result)
|
||||
assert.equals(0, #result)
|
||||
end)
|
||||
|
||||
it('handles problem completion for platform contest', function()
|
||||
package.loaded['cp.cache'] = {
|
||||
load = function() end,
|
||||
get_contest_data = function(platform, contest)
|
||||
if platform == 'atcoder' and contest == 'abc350' then
|
||||
return {
|
||||
problems = {
|
||||
{ id = 'a' },
|
||||
{ id = 'b' },
|
||||
},
|
||||
}
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
|
||||
local result = complete_fn('a', 'CP atcoder abc350 a', 18)
|
||||
|
||||
assert.is_table(result)
|
||||
assert.equals(1, #result)
|
||||
assert.equals('a', result[1])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
describe('cp.config', function()
|
||||
local config
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
config = require('cp.config')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('setup', function()
|
||||
it('returns defaults with nil input', function()
|
||||
local result = config.setup()
|
||||
|
||||
assert.equals('table', type(result.contests))
|
||||
assert.equals('table', type(result.snippets))
|
||||
assert.equals('table', type(result.hooks))
|
||||
assert.equals('table', type(result.scrapers))
|
||||
assert.is_false(result.debug)
|
||||
assert.is_nil(result.filename)
|
||||
end)
|
||||
|
||||
it('merges user config with defaults', function()
|
||||
local user_config = {
|
||||
debug = true,
|
||||
contests = { test_contest = { cpp = { extension = 'cpp' } } },
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.is_true(result.debug)
|
||||
assert.equals('table', type(result.contests.test_contest))
|
||||
assert.equals('table', type(result.scrapers))
|
||||
end)
|
||||
|
||||
it('allows custom extensions', function()
|
||||
local custom_config = {
|
||||
contests = {
|
||||
test_contest = {
|
||||
cpp = { extension = 'custom' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
config.setup(custom_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates scraper platforms', function()
|
||||
local invalid_config = {
|
||||
scrapers = { 'invalid_platform' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates scraper values are strings', function()
|
||||
local invalid_config = {
|
||||
scrapers = { 123 },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates diff_mode values', function()
|
||||
local valid_config = {
|
||||
run_panel = {
|
||||
diff_mode = 'none',
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
config.setup(valid_config)
|
||||
end)
|
||||
|
||||
local invalid_config = {
|
||||
run_panel = {
|
||||
diff_mode = 'invalid_mode',
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates hook functions', function()
|
||||
local invalid_config = {
|
||||
hooks = { before_run = 'not_a_function' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('run_panel config validation', function()
|
||||
it('validates ansi is boolean', function()
|
||||
local invalid_config = {
|
||||
run_panel = { ansi = 'invalid' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end, 'ansi: expected ansi color parsing must be enabled xor disabled, got string')
|
||||
end)
|
||||
|
||||
it('validates diff_mode values', function()
|
||||
local invalid_config = {
|
||||
run_panel = { diff_mode = 'invalid' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates next_test_key is non-empty string', function()
|
||||
local invalid_config = {
|
||||
run_panel = { next_test_key = '' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates prev_test_key is non-empty string', function()
|
||||
local invalid_config = {
|
||||
run_panel = { prev_test_key = '' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('accepts valid run_panel config', function()
|
||||
local valid_config = {
|
||||
run_panel = {
|
||||
ansi = false,
|
||||
diff_mode = 'git',
|
||||
next_test_key = 'j',
|
||||
prev_test_key = 'k',
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
config.setup(valid_config)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('auto-configuration', function()
|
||||
it('sets default extensions for cpp and python', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
cpp = { compile = { 'g++' } },
|
||||
python = { test = { 'python3' } },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.equals('cpp', result.contests.test.cpp.extension)
|
||||
assert.equals('py', result.contests.test.python.extension)
|
||||
end)
|
||||
|
||||
it('sets default_language to cpp when available', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
cpp = { compile = { 'g++' } },
|
||||
python = { test = { 'python3' } },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.equals('cpp', result.contests.test.default_language)
|
||||
end)
|
||||
|
||||
it('sets default_language to single available language when only one configured', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
python = { test = { 'python3' } },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.equals('python', result.contests.test.default_language)
|
||||
end)
|
||||
|
||||
it('sets default_language to single available language even when not cpp', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
rust = {
|
||||
test = { './target/release/solution' },
|
||||
extension = 'rs',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.equals('rust', result.contests.test.default_language)
|
||||
end)
|
||||
|
||||
it('uses first available language when multiple configured', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
python = { test = { 'python3' } },
|
||||
cpp = { compile = { 'g++' } },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.is_true(vim.tbl_contains({ 'cpp', 'python' }, result.contests.test.default_language))
|
||||
end)
|
||||
|
||||
it('preserves explicit default_language', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
cpp = { compile = { 'g++' } },
|
||||
python = { test = { 'python3' } },
|
||||
default_language = 'python',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local result = config.setup(user_config)
|
||||
|
||||
assert.equals('python', result.contests.test.default_language)
|
||||
end)
|
||||
|
||||
it('errors when no language configurations exist', function()
|
||||
local invalid_config = {
|
||||
contests = {
|
||||
test = {},
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end, 'No language configurations found')
|
||||
end)
|
||||
|
||||
it('allows custom language names', function()
|
||||
local user_config = {
|
||||
contests = {
|
||||
test = {
|
||||
rust = {
|
||||
compile = { 'rustc', '{source}', '-o', '{binary}' },
|
||||
test = { '{binary}' },
|
||||
extension = 'rs',
|
||||
},
|
||||
cpp = { compile = { 'g++' } },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
local result = config.setup(user_config)
|
||||
assert.equals('cpp', result.contests.test.default_language)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('picker validation', function()
|
||||
it('validates picker is valid value', function()
|
||||
local invalid_config = {
|
||||
picker = 'invalid_picker',
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end, "Invalid picker 'invalid_picker'. Must be 'telescope' or 'fzf-lua'")
|
||||
end)
|
||||
|
||||
it('allows nil picker', function()
|
||||
assert.has_no.errors(function()
|
||||
local result = config.setup({ picker = nil })
|
||||
assert.is_nil(result.picker)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('allows telescope picker without checking availability', function()
|
||||
assert.has_no.errors(function()
|
||||
local result = config.setup({ picker = 'telescope' })
|
||||
assert.equals('telescope', result.picker)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('allows fzf-lua picker without checking availability', function()
|
||||
assert.has_no.errors(function()
|
||||
local result = config.setup({ picker = 'fzf-lua' })
|
||||
assert.equals('fzf-lua', result.picker)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('default_filename', function()
|
||||
it('generates lowercase contest filename', function()
|
||||
local result = config.default_filename('ABC123')
|
||||
assert.equals('abc123', result)
|
||||
end)
|
||||
|
||||
it('combines contest and problem ids', function()
|
||||
local result = config.default_filename('ABC123', 'A')
|
||||
assert.equals('abc123a', result)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
describe('cp.diff', function()
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
local diff
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
diff = require('cp.ui.diff')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('get_available_backends', function()
|
||||
it('returns none, vim and git backends', function()
|
||||
local backends = diff.get_available_backends()
|
||||
table.sort(backends)
|
||||
assert.same({ 'git', 'none', 'vim' }, backends)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_backend', function()
|
||||
it('returns vim backend by name', function()
|
||||
local backend = diff.get_backend('vim')
|
||||
assert.is_not_nil(backend)
|
||||
assert.equals('vim', backend.name)
|
||||
end)
|
||||
|
||||
it('returns git backend by name', function()
|
||||
local backend = diff.get_backend('git')
|
||||
assert.is_not_nil(backend)
|
||||
assert.equals('git', backend.name)
|
||||
end)
|
||||
|
||||
it('returns none backend by name', function()
|
||||
local backend = diff.get_backend('none')
|
||||
assert.is_not_nil(backend)
|
||||
assert.equals('none', backend.name)
|
||||
end)
|
||||
|
||||
it('returns nil for invalid name', function()
|
||||
local backend = diff.get_backend('invalid')
|
||||
assert.is_nil(backend)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_best_backend', function()
|
||||
it('defaults to vim backend', function()
|
||||
local backend = diff.get_best_backend()
|
||||
assert.equals('vim', backend.name)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('none backend', function()
|
||||
it('returns both expected and actual content', function()
|
||||
local backend = diff.get_backend('none')
|
||||
local result = backend.render('expected\nline2', 'actual\nline2')
|
||||
|
||||
assert.same({
|
||||
expected = { 'expected', 'line2' },
|
||||
actual = { 'actual', 'line2' },
|
||||
}, result.content)
|
||||
assert.same({}, result.highlights)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('vim backend', function()
|
||||
it('returns content as-is', function()
|
||||
local backend = diff.get_backend('vim')
|
||||
local result = backend.render('expected', 'actual')
|
||||
|
||||
assert.same({ 'actual' }, result.content)
|
||||
assert.is_nil(result.highlights)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('is_git_available', function()
|
||||
it('returns boolean without errors', function()
|
||||
local result = diff.is_git_available()
|
||||
assert.equals('boolean', type(result))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('render_diff', function()
|
||||
it('returns result without errors', function()
|
||||
assert.has_no_errors(function()
|
||||
diff.render_diff('expected', 'actual', 'vim')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
describe('Error boundary handling', function()
|
||||
local cp
|
||||
local state
|
||||
local logged_messages
|
||||
|
||||
before_each(function()
|
||||
logged_messages = {}
|
||||
local mock_logger = {
|
||||
log = function(msg, level)
|
||||
table.insert(logged_messages, { msg = msg, level = level })
|
||||
end,
|
||||
set_config = function() end,
|
||||
}
|
||||
package.loaded['cp.log'] = mock_logger
|
||||
|
||||
package.loaded['cp.scraper'] = {
|
||||
scrape_problem_tests = function(_, contest_id, problem_id, callback)
|
||||
if contest_id == 'fail_scrape' then
|
||||
callback({
|
||||
success = false,
|
||||
error = 'Network error',
|
||||
})
|
||||
return
|
||||
end
|
||||
callback({
|
||||
success = true,
|
||||
problem_id = problem_id,
|
||||
tests = {
|
||||
{ input = '1', expected = '2' },
|
||||
},
|
||||
})
|
||||
end,
|
||||
scrape_contest_metadata = function(_, contest_id, callback)
|
||||
if contest_id == 'fail_scrape' then
|
||||
callback({
|
||||
success = false,
|
||||
error = 'Network error',
|
||||
})
|
||||
return
|
||||
end
|
||||
if contest_id == 'fail_metadata' then
|
||||
callback({
|
||||
success = false,
|
||||
error = 'Contest not found',
|
||||
})
|
||||
return
|
||||
end
|
||||
callback({
|
||||
success = true,
|
||||
problems = {
|
||||
{ id = 'a' },
|
||||
{ id = 'b' },
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
cache.load = function() end
|
||||
cache.set_test_cases = function() end
|
||||
cache.set_file_state = function() end
|
||||
cache.get_file_state = function()
|
||||
return nil
|
||||
end
|
||||
cache.get_contest_data = function()
|
||||
return nil
|
||||
end
|
||||
cache.get_test_cases = function()
|
||||
return {}
|
||||
end
|
||||
|
||||
if not vim.fn then
|
||||
vim.fn = {}
|
||||
end
|
||||
vim.fn.expand = vim.fn.expand or function()
|
||||
return '/tmp/test.cpp'
|
||||
end
|
||||
vim.fn.mkdir = vim.fn.mkdir or function() end
|
||||
if not vim.api then
|
||||
vim.api = {}
|
||||
end
|
||||
vim.api.nvim_get_current_buf = vim.api.nvim_get_current_buf or function()
|
||||
return 1
|
||||
end
|
||||
vim.api.nvim_buf_get_lines = vim.api.nvim_buf_get_lines
|
||||
or function()
|
||||
return { '' }
|
||||
end
|
||||
if not vim.cmd then
|
||||
vim.cmd = {}
|
||||
end
|
||||
vim.cmd.e = function() end
|
||||
vim.cmd.only = function() end
|
||||
if not vim.system then
|
||||
vim.system = function(_)
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
state = require('cp.state')
|
||||
state.reset()
|
||||
|
||||
cp = require('cp')
|
||||
cp.setup({
|
||||
contests = {
|
||||
codeforces = {
|
||||
default_language = 'cpp',
|
||||
cpp = { extension = 'cpp', test = { 'echo', 'test' } },
|
||||
},
|
||||
},
|
||||
scrapers = { 'codeforces' },
|
||||
})
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
package.loaded['cp.log'] = nil
|
||||
package.loaded['cp.scraper'] = nil
|
||||
if state then
|
||||
state.reset()
|
||||
end
|
||||
end)
|
||||
|
||||
it('should handle scraping failures without state corruption', function()
|
||||
cp.handle_command({ fargs = { 'codeforces', 'fail_scrape', 'a' } })
|
||||
|
||||
vim.wait(100)
|
||||
|
||||
local has_metadata_error = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.msg and log_entry.msg:match('failed to load contest metadata') then
|
||||
has_metadata_error = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(has_metadata_error, 'Should log contest metadata failure')
|
||||
|
||||
assert.equals('codeforces', state.get_platform())
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'run' } })
|
||||
end)
|
||||
end)
|
||||
|
||||
it('should handle missing contest data without crashing navigation', function()
|
||||
state.set_platform('codeforces')
|
||||
state.set_contest_id('nonexistent')
|
||||
state.set_problem_id('a')
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'next' } })
|
||||
end)
|
||||
|
||||
local has_nav_error = false
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.msg and log_entry.msg:match('no contest data available') then
|
||||
has_nav_error = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(has_nav_error, 'Should log navigation error')
|
||||
end)
|
||||
|
||||
it('should handle validation errors without crashing', function()
|
||||
state.reset()
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'next' } })
|
||||
end)
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'prev' } })
|
||||
end)
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'run' } })
|
||||
end)
|
||||
|
||||
local has_validation_error = false
|
||||
local has_appropriate_errors = 0
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if log_entry.msg and log_entry.msg:match('expected string, got nil') then
|
||||
has_validation_error = true
|
||||
elseif
|
||||
log_entry.msg
|
||||
and (log_entry.msg:match('No platform ') or log_entry.msg:match('No contest '))
|
||||
then
|
||||
has_appropriate_errors = has_appropriate_errors + 1
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_false(has_validation_error, 'Should not have validation errors')
|
||||
assert.is_true(has_appropriate_errors > 0, 'Should have user-facing errors')
|
||||
end)
|
||||
|
||||
it('should handle partial state gracefully', function()
|
||||
state.set_platform('codeforces')
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'run' } })
|
||||
end)
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'next' } })
|
||||
end)
|
||||
|
||||
local missing_contest_errors = 0
|
||||
for _, log_entry in ipairs(logged_messages) do
|
||||
if
|
||||
log_entry.msg
|
||||
and (log_entry.msg:match('No problem found') or log_entry.msg:match('No contest'))
|
||||
then
|
||||
missing_contest_errors = missing_contest_errors + 1
|
||||
end
|
||||
end
|
||||
assert.is_true(missing_contest_errors > 0, 'Should report missing contest')
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,429 +1,11 @@
|
|||
describe('cp.execute', function()
|
||||
local execute
|
||||
local mock_system_calls
|
||||
local temp_files
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
execute = require('cp.runner.execute')
|
||||
mock_system_calls = {}
|
||||
temp_files = {}
|
||||
|
||||
vim.system = function(cmd, opts)
|
||||
table.insert(mock_system_calls, { cmd = cmd, opts = opts })
|
||||
if not cmd or #cmd == 0 then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = '', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local result = { code = 0, stdout = '', stderr = '' }
|
||||
|
||||
if cmd[1] == 'mkdir' then
|
||||
result = { code = 0 }
|
||||
elseif cmd[1] == 'g++' or cmd[1] == 'gcc' then
|
||||
result = { code = 0, stderr = '' }
|
||||
elseif cmd[1]:match('%.run$') or cmd[1] == 'python' then
|
||||
result = { code = 0, stdout = '42\n', stderr = '' }
|
||||
end
|
||||
|
||||
return {
|
||||
wait = function()
|
||||
return result
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local original_fn = vim.fn
|
||||
vim.fn = vim.tbl_extend('force', vim.fn, {
|
||||
filereadable = function(path)
|
||||
return temp_files[path] and 1 or 0
|
||||
end,
|
||||
readfile = function(path)
|
||||
return temp_files[path] or {}
|
||||
end,
|
||||
fnamemodify = function(path, modifier)
|
||||
if modifier == ':e' then
|
||||
return path:match('%.([^.]+)$') or ''
|
||||
end
|
||||
return original_fn.fnamemodify(path, modifier)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.uv = vim.tbl_extend('force', vim.uv or {}, {
|
||||
hrtime = function()
|
||||
return 1000000000
|
||||
end,
|
||||
})
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.system = vim.system_original or vim.system
|
||||
spec_helper.teardown()
|
||||
temp_files = {}
|
||||
end)
|
||||
|
||||
describe('template substitution', function()
|
||||
it('substitutes placeholders correctly', function()
|
||||
local language_config = {
|
||||
compile = { 'g++', '{source_file}', '-o', '{binary_file}' },
|
||||
}
|
||||
local substitutions = {
|
||||
source_file = 'test.cpp',
|
||||
binary_file = 'test.run',
|
||||
}
|
||||
|
||||
local result = execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.equals(0, result.code)
|
||||
assert.is_true(#mock_system_calls > 0)
|
||||
|
||||
local compile_call = mock_system_calls[1]
|
||||
assert.equals('sh', compile_call.cmd[1])
|
||||
assert.equals('-c', compile_call.cmd[2])
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], 'g%+%+ test%.cpp %-o test%.run'))
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], '2>&1'))
|
||||
end)
|
||||
|
||||
it('handles multiple substitutions in single argument', function()
|
||||
local language_config = {
|
||||
compile = { 'g++', '{source_file}', '-o{binary_file}' },
|
||||
}
|
||||
local substitutions = {
|
||||
source_file = 'main.cpp',
|
||||
binary_file = 'main.out',
|
||||
}
|
||||
|
||||
execute.compile_generic(language_config, substitutions)
|
||||
|
||||
local compile_call = mock_system_calls[1]
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], '%-omain%.out'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('compilation', function()
|
||||
it('skips compilation when not required', function()
|
||||
local language_config = {}
|
||||
local substitutions = {}
|
||||
|
||||
local result = execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.equals(0, result.code)
|
||||
assert.equals('', result.stderr)
|
||||
assert.equals(0, #mock_system_calls)
|
||||
end)
|
||||
|
||||
it('compiles cpp files correctly', function()
|
||||
local language_config = {
|
||||
compile = { 'g++', '{source_file}', '-o', '{binary_file}', '-std=c++17' },
|
||||
}
|
||||
local substitutions = {
|
||||
source_file = 'solution.cpp',
|
||||
binary_file = 'build/solution.run',
|
||||
}
|
||||
|
||||
local result = execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.equals(0, result.code)
|
||||
assert.is_true(#mock_system_calls > 0)
|
||||
|
||||
local compile_call = mock_system_calls[1]
|
||||
assert.equals('sh', compile_call.cmd[1])
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], '%-std=c%+%+17'))
|
||||
end)
|
||||
|
||||
it('handles compilation errors gracefully', function()
|
||||
vim.system = function()
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stderr = 'error: undefined variable' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'g++', '{source_file}', '-o', '{binary_file}' },
|
||||
}
|
||||
local substitutions = { source_file = 'bad.cpp', binary_file = 'bad.run' }
|
||||
|
||||
local result = execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.equals(1, result.code)
|
||||
assert.is_not_nil(result.stderr:match('undefined variable'))
|
||||
end)
|
||||
|
||||
it('measures compilation time', function()
|
||||
local start_time = 1000000000
|
||||
local end_time = 1500000000
|
||||
local call_count = 0
|
||||
|
||||
vim.uv.hrtime = function()
|
||||
call_count = call_count + 1
|
||||
if call_count == 1 then
|
||||
return start_time
|
||||
else
|
||||
return end_time
|
||||
end
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'g++', 'test.cpp', '-o', 'test.run' },
|
||||
}
|
||||
|
||||
execute.compile_generic(language_config, {})
|
||||
assert.is_true(call_count >= 2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('test execution', function()
|
||||
it('executes commands with input data', function()
|
||||
vim.system = function(cmd, opts)
|
||||
table.insert(mock_system_calls, { cmd = cmd, opts = opts })
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = '3\n', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
run = { '{binary_file}' },
|
||||
}
|
||||
|
||||
execute.compile_generic(language_config, { binary_file = './test.run' })
|
||||
end)
|
||||
|
||||
it('handles command execution', function()
|
||||
vim.system = function(_, opts)
|
||||
if opts then
|
||||
assert.equals(false, opts.text)
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 124, stdout = '', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'timeout', '1', 'sleep', '2' },
|
||||
}
|
||||
|
||||
local result = execute.compile_generic(language_config, {})
|
||||
assert.equals(124, result.code)
|
||||
end)
|
||||
|
||||
it('captures stderr output', function()
|
||||
vim.system = function()
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stdout = '', stderr = 'runtime error\n' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'false' },
|
||||
}
|
||||
|
||||
local result = execute.compile_generic(language_config, {})
|
||||
assert.equals(1, result.code)
|
||||
assert.is_not_nil(result.stderr:match('runtime error'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('directory creation', function()
|
||||
it('creates build and io directories', function()
|
||||
local language_config = {
|
||||
compile = { 'mkdir', '-p', 'build', 'io' },
|
||||
}
|
||||
|
||||
execute.compile_generic(language_config, {})
|
||||
|
||||
local mkdir_call = mock_system_calls[1]
|
||||
assert.equals('sh', mkdir_call.cmd[1])
|
||||
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'mkdir'))
|
||||
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'build'))
|
||||
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'io'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('language detection', function()
|
||||
it('detects cpp from extension', function()
|
||||
vim.fn.fnamemodify = function()
|
||||
return 'cpp'
|
||||
end
|
||||
|
||||
assert.has_no_errors(function()
|
||||
execute.compile_generic({}, {})
|
||||
end)
|
||||
end)
|
||||
|
||||
it('falls back to default language', function()
|
||||
vim.fn.fnamemodify = function(_, modifier)
|
||||
if modifier == ':e' then
|
||||
return 'unknown'
|
||||
end
|
||||
return ''
|
||||
end
|
||||
|
||||
assert.has_no_errors(function()
|
||||
execute.compile_generic({}, {})
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('edge cases', function()
|
||||
it('handles empty command templates', function()
|
||||
local language_config = {
|
||||
compile = {},
|
||||
}
|
||||
|
||||
local result = execute.compile_generic(language_config, {})
|
||||
assert.equals(0, result.code)
|
||||
end)
|
||||
|
||||
it('handles commands with no substitutions needed', function()
|
||||
local language_config = {
|
||||
compile = { 'echo', 'hello' },
|
||||
}
|
||||
|
||||
local result = execute.compile_generic(language_config, {})
|
||||
assert.equals(0, result.code)
|
||||
|
||||
local echo_call = mock_system_calls[1]
|
||||
assert.equals('sh', echo_call.cmd[1])
|
||||
assert.is_not_nil(string.find(echo_call.cmd[3], 'echo hello'))
|
||||
end)
|
||||
|
||||
it('handles multiple consecutive substitutions', function()
|
||||
local language_config = {
|
||||
compile = { '{compiler}{compiler}', '{file}{file}' },
|
||||
}
|
||||
local substitutions = {
|
||||
compiler = 'g++',
|
||||
file = 'test.cpp',
|
||||
}
|
||||
|
||||
execute.compile_generic(language_config, substitutions)
|
||||
|
||||
local call = mock_system_calls[1]
|
||||
assert.equals('sh', call.cmd[1])
|
||||
assert.is_not_nil(string.find(call.cmd[3], 'g%+%+g%+%+ test%.cpptest%.cpp'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('stderr/stdout redirection', function()
|
||||
it('should use stderr redirection (2>&1)', function()
|
||||
local original_system = vim.system
|
||||
local captured_command = nil
|
||||
|
||||
vim.system = function(cmd, _)
|
||||
captured_command = cmd
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = '', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'g++', '-std=c++17', '-o', '{binary}', '{source}' },
|
||||
}
|
||||
local substitutions = { source = 'test.cpp', binary = 'build/test', version = '17' }
|
||||
execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.is_not_nil(captured_command)
|
||||
assert.equals('sh', captured_command[1])
|
||||
assert.equals('-c', captured_command[2])
|
||||
assert.is_not_nil(
|
||||
string.find(captured_command[3], '2>&1'),
|
||||
'Command should contain 2>&1 redirection'
|
||||
)
|
||||
|
||||
vim.system = original_system
|
||||
end)
|
||||
|
||||
it('should return combined stdout+stderr in result', function()
|
||||
local original_system = vim.system
|
||||
local test_output = 'STDOUT: Hello\nSTDERR: Error message\n'
|
||||
|
||||
vim.system = function(_, _)
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stdout = test_output, stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'g++', '-std=c++17', '-o', '{binary}', '{source}' },
|
||||
}
|
||||
local substitutions = { source = 'test.cpp', binary = 'build/test', version = '17' }
|
||||
local result = execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.equals(1, result.code)
|
||||
assert.equals(test_output, result.stdout)
|
||||
|
||||
vim.system = original_system
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('integration with execute_command function', function()
|
||||
it('tests the full execute_command flow with stderr/stdout combination', function()
|
||||
local cmd = { 'echo', 'test output' }
|
||||
local input_data = 'test input'
|
||||
local timeout_ms = 1000
|
||||
|
||||
local original_system = vim.system
|
||||
vim.system = function(shell_cmd, opts)
|
||||
assert.equals('sh', shell_cmd[1])
|
||||
assert.equals('-c', shell_cmd[2])
|
||||
assert.is_not_nil(string.find(shell_cmd[3], '2>&1'))
|
||||
assert.equals(input_data, opts.stdin)
|
||||
assert.equals(timeout_ms, opts.timeout)
|
||||
assert.is_true(opts.text)
|
||||
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = 'combined output from stdout and stderr', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local execute_command = require('cp.runner.execute').execute_command
|
||||
or function(command, stdin_data, timeout)
|
||||
local redirected_cmd = vim.deepcopy(command)
|
||||
if #redirected_cmd > 0 then
|
||||
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
|
||||
end
|
||||
|
||||
local result = vim
|
||||
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, {
|
||||
stdin = stdin_data,
|
||||
timeout = timeout,
|
||||
text = true,
|
||||
})
|
||||
:wait()
|
||||
|
||||
return {
|
||||
stdout = result.stdout or '',
|
||||
stderr = result.stderr or '',
|
||||
code = result.code or 0,
|
||||
time_ms = 0,
|
||||
timed_out = result.code == 124,
|
||||
}
|
||||
end
|
||||
|
||||
local result = execute_command(cmd, input_data, timeout_ms)
|
||||
|
||||
assert.equals(0, result.code)
|
||||
assert.equals('combined output from stdout and stderr', result.stdout)
|
||||
|
||||
vim.system = original_system
|
||||
describe('run module', function()
|
||||
local run = require('cp.runner.run')
|
||||
|
||||
describe('basic functionality', function()
|
||||
it('can get panel state', function()
|
||||
local state = run.get_run_panel_state()
|
||||
assert.is_table(state)
|
||||
assert.is_table(state.test_cases)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
describe('cp.fzf_lua', function()
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
|
||||
package.preload['fzf-lua'] = function()
|
||||
return {
|
||||
fzf_exec = function(_, _) end,
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('module loading', function()
|
||||
it('loads fzf-lua integration without error', function()
|
||||
assert.has_no_errors(function()
|
||||
require('cp.pickers.fzf_lua')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('returns module with picker function', function()
|
||||
local fzf_lua_cp = require('cp.pickers.fzf_lua')
|
||||
assert.is_table(fzf_lua_cp)
|
||||
assert.is_function(fzf_lua_cp.pick)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('basic running', function()
|
||||
it('can run and open the picker with :CP pick', function()
|
||||
local cp = require('cp')
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'pick' } })
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
describe('cp.highlight', function()
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
local highlight
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
highlight = require('cp.ui.highlight')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('parse_git_diff', function()
|
||||
it('skips git diff headers', function()
|
||||
local diff_output = [[diff --git a/test b/test
|
||||
index 1234567..abcdefg 100644
|
||||
--- a/test
|
||||
+++ b/test
|
||||
@@ -1,3 +1,3 @@
|
||||
hello
|
||||
+world
|
||||
-goodbye]]
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'hello', 'world' }, result.content)
|
||||
end)
|
||||
|
||||
it('processes added lines', function()
|
||||
local diff_output = '+hello w{+o+}rld'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'hello world' }, result.content)
|
||||
assert.equals(1, #result.highlights)
|
||||
assert.equals('CpDiffAdded', result.highlights[1].highlight_group)
|
||||
end)
|
||||
|
||||
it('ignores removed lines', function()
|
||||
local diff_output = 'hello\n-removed line\n+kept line'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'hello', 'kept line' }, result.content)
|
||||
end)
|
||||
|
||||
it('handles unchanged lines', function()
|
||||
local diff_output = 'unchanged line\n+added line'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'unchanged line', 'added line' }, result.content)
|
||||
end)
|
||||
|
||||
it('sets correct line numbers', function()
|
||||
local diff_output = '+first {+added+}\n+second {+text+}'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.equals(0, result.highlights[1].line)
|
||||
assert.equals(1, result.highlights[2].line)
|
||||
end)
|
||||
|
||||
it('handles empty diff output', function()
|
||||
local result = highlight.parse_git_diff('')
|
||||
assert.same({}, result.content)
|
||||
assert.same({}, result.highlights)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('apply_highlights', function()
|
||||
it('handles empty highlights without errors', function()
|
||||
local namespace = highlight.create_namespace()
|
||||
assert.has_no_errors(function()
|
||||
highlight.apply_highlights(1, {}, namespace)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles valid highlight data without errors', function()
|
||||
vim.api.nvim_buf_set_lines(1, 0, -1, false, { 'hello world test line' })
|
||||
local highlights = {
|
||||
{
|
||||
line = 0,
|
||||
col_start = 5,
|
||||
col_end = 10,
|
||||
highlight_group = 'CpDiffAdded',
|
||||
},
|
||||
}
|
||||
local namespace = highlight.create_namespace()
|
||||
assert.has_no_errors(function()
|
||||
highlight.apply_highlights(1, highlights, namespace)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('create_namespace', function()
|
||||
it('returns a number', function()
|
||||
local result = highlight.create_namespace()
|
||||
assert.equals('number', type(result))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse_and_apply_diff', function()
|
||||
it('returns content lines', function()
|
||||
local namespace = highlight.create_namespace()
|
||||
local result = highlight.parse_and_apply_diff(1, '+first\n+second', namespace)
|
||||
assert.same({ 'first', 'second' }, result)
|
||||
end)
|
||||
|
||||
it('handles empty diff', function()
|
||||
local namespace = highlight.create_namespace()
|
||||
local result = highlight.parse_and_apply_diff(1, '', namespace)
|
||||
assert.same({}, result)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
describe('Panel integration', function()
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
local cp
|
||||
local state
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup_full()
|
||||
spec_helper.mock_scraper_success()
|
||||
|
||||
state = require('cp.state')
|
||||
state.reset()
|
||||
|
||||
local mock_async_setup = {
|
||||
setup_contest_async = function() end,
|
||||
handle_full_setup_async = function(cmd)
|
||||
state.set_platform(cmd.platform)
|
||||
state.set_contest_id(cmd.contest)
|
||||
state.set_problem_id(cmd.problem)
|
||||
end,
|
||||
setup_problem_async = function() end,
|
||||
}
|
||||
package.loaded['cp.async.setup'] = mock_async_setup
|
||||
|
||||
local mock_setup = {
|
||||
set_platform = function(platform)
|
||||
state.set_platform(platform)
|
||||
return true
|
||||
end,
|
||||
setup_contest = function(platform, contest, problem, _)
|
||||
state.set_platform(platform)
|
||||
state.set_contest_id(contest)
|
||||
if problem then
|
||||
state.set_problem_id(problem)
|
||||
end
|
||||
end,
|
||||
setup_problem = function() end,
|
||||
navigate_problem = function() end,
|
||||
}
|
||||
package.loaded['cp.setup'] = mock_setup
|
||||
|
||||
cp = require('cp')
|
||||
cp.setup({
|
||||
contests = {
|
||||
codeforces = {
|
||||
default_language = 'cpp',
|
||||
cpp = { extension = 'cpp', test = { 'echo', 'test' } },
|
||||
},
|
||||
},
|
||||
scrapers = { 'codeforces' },
|
||||
})
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
if state then
|
||||
state.reset()
|
||||
end
|
||||
end)
|
||||
|
||||
it('should handle run command with properly set contest context', function()
|
||||
cp.handle_command({ fargs = { 'codeforces', '2146', 'b' } })
|
||||
|
||||
assert.equals('codeforces', state.get_platform())
|
||||
assert.equals('2146', state.get_contest_id())
|
||||
assert.equals('b', state.get_problem_id())
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'run' } })
|
||||
end)
|
||||
|
||||
local has_validation_error = false
|
||||
for _, log_entry in ipairs(spec_helper.logged_messages) do
|
||||
if
|
||||
log_entry.level == vim.log.levels.ERROR
|
||||
and log_entry.msg:match('expected string, got nil')
|
||||
then
|
||||
has_validation_error = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_false(has_validation_error)
|
||||
end)
|
||||
|
||||
it('should handle state module interface correctly', function()
|
||||
local run = require('cp.runner.run')
|
||||
|
||||
state.set_platform('codeforces')
|
||||
state.set_contest_id('2146')
|
||||
state.set_problem_id('b')
|
||||
|
||||
local config_module = require('cp.config')
|
||||
config_module.setup({
|
||||
contests = { codeforces = { cpp = { extension = 'cpp' } } },
|
||||
})
|
||||
local cp_state = require('cp.state')
|
||||
cp_state.set_platform('codeforces')
|
||||
cp_state.set_contest_id('2146')
|
||||
cp_state.set_problem_id('b')
|
||||
|
||||
assert.has_no_errors(function()
|
||||
run.load_test_cases(state)
|
||||
end)
|
||||
|
||||
local fake_state_data = { platform = 'codeforces', contest_id = '2146', problem_id = 'b' }
|
||||
assert.has_errors(function()
|
||||
run.load_test_cases(fake_state_data)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
describe('cp.picker', function()
|
||||
local picker
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
picker = require('cp.pickers')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('get_contests_for_platform', function()
|
||||
it('returns empty list when scraper fails', function()
|
||||
vim.system = function(_, _)
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stderr = 'test error' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local contests = picker.get_contests_for_platform('test_platform')
|
||||
assert.is_table(contests)
|
||||
assert.equals(0, #contests)
|
||||
end)
|
||||
|
||||
it('returns empty list when JSON is invalid', function()
|
||||
vim.system = function(_, _)
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = 'invalid json' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local contests = picker.get_contests_for_platform('test_platform')
|
||||
assert.is_table(contests)
|
||||
assert.equals(0, #contests)
|
||||
end)
|
||||
|
||||
it('returns contest list when scraper succeeds', function()
|
||||
local cache = require('cp.cache')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
cache.load = function() end
|
||||
cache.get_contest_list = function()
|
||||
return nil
|
||||
end
|
||||
cache.set_contest_list = function() end
|
||||
|
||||
utils.setup_python_env = function()
|
||||
return true
|
||||
end
|
||||
utils.get_plugin_path = function()
|
||||
return '/test/path'
|
||||
end
|
||||
|
||||
vim.system = function(_, _)
|
||||
return {
|
||||
wait = function()
|
||||
return {
|
||||
code = 0,
|
||||
stdout = vim.json.encode({
|
||||
success = true,
|
||||
contests = {
|
||||
{
|
||||
id = 'abc123',
|
||||
name = 'AtCoder Beginner Contest 123',
|
||||
display_name = 'Beginner Contest 123 (ABC)',
|
||||
},
|
||||
{
|
||||
id = '1951',
|
||||
name = 'Educational Round 168',
|
||||
display_name = 'Educational Round 168',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local contests = picker.get_contests_for_platform('test_platform')
|
||||
assert.is_table(contests)
|
||||
assert.equals(2, #contests)
|
||||
assert.equals('abc123', contests[1].id)
|
||||
assert.equals('AtCoder Beginner Contest 123', contests[1].name)
|
||||
assert.equals('Beginner Contest 123 (ABC)', contests[1].display_name)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_problems_for_contest', function()
|
||||
it('returns problems from cache when available', function()
|
||||
local cache = require('cp.cache')
|
||||
cache.load = function() end
|
||||
cache.get_contest_data = function(_, _)
|
||||
return {
|
||||
problems = {
|
||||
{ id = 'a', name = 'Problem A' },
|
||||
{ id = 'b', name = 'Problem B' },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local problems = picker.get_problems_for_contest('test_platform', 'test_contest')
|
||||
assert.is_table(problems)
|
||||
assert.equals(2, #problems)
|
||||
assert.equals('a', problems[1].id)
|
||||
assert.equals('Problem A', problems[1].name)
|
||||
assert.equals('Problem A', problems[1].display_name)
|
||||
end)
|
||||
|
||||
it('falls back to scraping when cache miss', function()
|
||||
local cache = require('cp.cache')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
cache.load = function() end
|
||||
cache.get_contest_data = function(_, _)
|
||||
return nil
|
||||
end
|
||||
cache.set_contest_data = function() end
|
||||
|
||||
utils.setup_python_env = function()
|
||||
return true
|
||||
end
|
||||
utils.get_plugin_path = function()
|
||||
return '/tmp'
|
||||
end
|
||||
|
||||
vim.system = function()
|
||||
return {
|
||||
wait = function()
|
||||
return {
|
||||
code = 0,
|
||||
stdout = vim.json.encode({
|
||||
success = true,
|
||||
problems = {
|
||||
{ id = 'x', name = 'Problem X' },
|
||||
},
|
||||
}),
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
picker = spec_helper.fresh_require('cp.pickers', { 'cp.pickers.init' })
|
||||
|
||||
local problems = picker.get_problems_for_contest('test_platform', 'test_contest')
|
||||
assert.is_table(problems)
|
||||
assert.equals(1, #problems)
|
||||
assert.equals('x', problems[1].id)
|
||||
end)
|
||||
|
||||
it('returns empty list when scraping fails', function()
|
||||
local cache = require('cp.cache')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
cache.load = function() end
|
||||
cache.get_contest_data = function(_, _)
|
||||
return nil
|
||||
end
|
||||
|
||||
utils.setup_python_env = function()
|
||||
return true
|
||||
end
|
||||
utils.get_plugin_path = function()
|
||||
return '/tmp'
|
||||
end
|
||||
|
||||
vim.system = function()
|
||||
return {
|
||||
wait = function()
|
||||
return {
|
||||
code = 1,
|
||||
stderr = 'Scraping failed',
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
picker = spec_helper.fresh_require('cp.pickers', { 'cp.pickers.init' })
|
||||
|
||||
local problems = picker.get_problems_for_contest('test_platform', 'test_contest')
|
||||
assert.is_table(problems)
|
||||
assert.equals(0, #problems)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
describe('cp.run_render', function()
|
||||
local run_render = require('cp.runner.run_render')
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('get_status_info', function()
|
||||
it('returns AC for pass status', function()
|
||||
local test_case = { status = 'pass' }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('AC', result.text)
|
||||
assert.equals('CpTestAC', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns WA for fail status with normal exit codes', function()
|
||||
local test_case = { status = 'fail', code = 1 }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('WA', result.text)
|
||||
assert.equals('CpTestWA', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns TLE for timeout status', function()
|
||||
local test_case = { status = 'timeout' }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('TLE', result.text)
|
||||
assert.equals('CpTestTLE', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns TLE for timed out fail status', function()
|
||||
local test_case = { status = 'fail', timed_out = true }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('TLE', result.text)
|
||||
assert.equals('CpTestTLE', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns RTE for fail with signal codes (>= 128)', function()
|
||||
local test_case = { status = 'fail', code = 139 }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('RTE', result.text)
|
||||
assert.equals('CpTestRTE', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns empty for pending status', function()
|
||||
local test_case = { status = 'pending' }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('', result.text)
|
||||
assert.equals('CpTestPending', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns running indicator for running status', function()
|
||||
local test_case = { status = 'running' }
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('...', result.text)
|
||||
assert.equals('CpTestPending', result.highlight_group)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('render_test_list', function()
|
||||
it('renders table with headers and borders', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '5' },
|
||||
{ status = 'fail', code = 1, input = '3' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = run_render.render_test_list(test_state)
|
||||
assert.is_true(result[1]:find('^┌') ~= nil)
|
||||
assert.is_true(result[2]:find('│.*#.*│.*Status.*│.*Time.*│.*Exit Code.*│') ~= nil)
|
||||
assert.is_true(result[3]:find('^├') ~= nil)
|
||||
end)
|
||||
|
||||
it('shows current test with > prefix in table', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '' },
|
||||
{ status = 'pass', input = '' },
|
||||
},
|
||||
current_index = 2,
|
||||
}
|
||||
local result = run_render.render_test_list(test_state)
|
||||
local found_current = false
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('│.*> 2.*│') then
|
||||
found_current = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(found_current)
|
||||
end)
|
||||
|
||||
it('displays input only for current test', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '5 3' },
|
||||
{ status = 'pass', input = '2 4' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = run_render.render_test_list(test_state)
|
||||
local found_input = false
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('│5 3') then
|
||||
found_input = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(found_input)
|
||||
end)
|
||||
|
||||
it('handles empty test cases', function()
|
||||
local test_state = { test_cases = {}, current_index = 1 }
|
||||
local result = run_render.render_test_list(test_state)
|
||||
assert.equals(3, #result)
|
||||
end)
|
||||
|
||||
it('preserves input line breaks', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '5\n3\n1' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = run_render.render_test_list(test_state)
|
||||
local input_lines = {}
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('^│[531]') then
|
||||
table.insert(input_lines, line:match('│([531])'))
|
||||
end
|
||||
end
|
||||
assert.same({ '5', '3', '1' }, input_lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('render_status_bar', function()
|
||||
it('formats time and exit code', function()
|
||||
local test_case = { time_ms = 45.7, code = 0 }
|
||||
local result = run_render.render_status_bar(test_case)
|
||||
assert.equals('45.70ms │ Exit: 0', result)
|
||||
end)
|
||||
|
||||
it('handles missing time', function()
|
||||
local test_case = { code = 0 }
|
||||
local result = run_render.render_status_bar(test_case)
|
||||
assert.equals('Exit: 0', result)
|
||||
end)
|
||||
|
||||
it('handles missing exit code', function()
|
||||
local test_case = { time_ms = 123 }
|
||||
local result = run_render.render_status_bar(test_case)
|
||||
assert.equals('123.00ms', result)
|
||||
end)
|
||||
|
||||
it('returns empty for nil test case', function()
|
||||
local result = run_render.render_status_bar(nil)
|
||||
assert.equals('', result)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('setup_highlights', function()
|
||||
it('runs without errors', function()
|
||||
assert.has_no_errors(function()
|
||||
run_render.setup_highlights()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('highlight positioning', function()
|
||||
it('generates correct highlight positions for status text', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '' },
|
||||
{ status = 'fail', code = 1, input = '' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local lines, highlights = run_render.render_test_list(test_state)
|
||||
|
||||
assert.equals(2, #highlights)
|
||||
|
||||
for _, hl in ipairs(highlights) do
|
||||
assert.is_not_nil(hl.line)
|
||||
assert.is_not_nil(hl.col_start)
|
||||
assert.is_not_nil(hl.col_end)
|
||||
assert.is_not_nil(hl.highlight_group)
|
||||
assert.is_true(hl.col_end > hl.col_start)
|
||||
|
||||
local line_content = lines[hl.line + 1]
|
||||
local highlighted_text = line_content:sub(hl.col_start + 1, hl.col_end)
|
||||
assert.is_true(highlighted_text == 'AC' or highlighted_text == 'WA')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
describe('run module', function()
|
||||
local run = require('cp.runner.run')
|
||||
|
||||
describe('basic functionality', function()
|
||||
it('has required functions', function()
|
||||
assert.is_function(run.load_test_cases)
|
||||
assert.is_function(run.run_test_case)
|
||||
assert.is_function(run.run_all_test_cases)
|
||||
assert.is_function(run.get_run_panel_state)
|
||||
assert.is_function(run.handle_compilation_failure)
|
||||
end)
|
||||
|
||||
it('can get panel state', function()
|
||||
local state = run.get_run_panel_state()
|
||||
assert.is_table(state)
|
||||
assert.is_table(state.test_cases)
|
||||
end)
|
||||
|
||||
it('handles compilation failure', function()
|
||||
local compilation_output = 'error.cpp:1:1: error: undefined variable'
|
||||
|
||||
assert.does_not_error(function()
|
||||
run.handle_compilation_failure(compilation_output)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
describe('cp.snippets', function()
|
||||
local snippets
|
||||
local mock_luasnip
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
snippets = spec_helper.fresh_require('cp.snippets')
|
||||
mock_luasnip = {
|
||||
snippet = function(trigger, body)
|
||||
return { trigger = trigger, body = body }
|
||||
end,
|
||||
insert_node = function(pos)
|
||||
return { type = 'insert', pos = pos }
|
||||
end,
|
||||
add_snippets = function(filetype, snippet_list)
|
||||
mock_luasnip.added = mock_luasnip.added or {}
|
||||
mock_luasnip.added[filetype] = snippet_list
|
||||
end,
|
||||
added = {},
|
||||
}
|
||||
|
||||
mock_luasnip.extras = {
|
||||
fmt = {
|
||||
fmt = function(template, nodes)
|
||||
return { template = template, nodes = nodes }
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
package.loaded['luasnip'] = mock_luasnip
|
||||
package.loaded['luasnip.extras.fmt'] = mock_luasnip.extras.fmt
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
package.loaded['cp.snippets'] = nil
|
||||
package.loaded['luasnip'] = nil
|
||||
package.loaded['luasnip.extras.fmt'] = nil
|
||||
end)
|
||||
|
||||
describe('setup without luasnip', function()
|
||||
it('handles missing luasnip gracefully', function()
|
||||
package.loaded['luasnip'] = nil
|
||||
|
||||
assert.has_no_errors(function()
|
||||
snippets.setup({})
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('setup with luasnip available', function()
|
||||
it('sets up default cpp snippets for all contests', function()
|
||||
local config = { snippets = {} }
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
assert.is_not_nil(mock_luasnip.added.cpp)
|
||||
assert.is_true(#mock_luasnip.added.cpp >= 3)
|
||||
|
||||
local triggers = {}
|
||||
for _, snippet in ipairs(mock_luasnip.added.cpp) do
|
||||
table.insert(triggers, snippet.trigger)
|
||||
end
|
||||
|
||||
assert.is_true(vim.tbl_contains(triggers, 'cp.nvim/codeforces.cpp'))
|
||||
assert.is_true(vim.tbl_contains(triggers, 'cp.nvim/atcoder.cpp'))
|
||||
assert.is_true(vim.tbl_contains(triggers, 'cp.nvim/cses.cpp'))
|
||||
end)
|
||||
|
||||
it('sets up default python snippets for all contests', function()
|
||||
local config = { snippets = {} }
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
assert.is_not_nil(mock_luasnip.added.python)
|
||||
assert.is_true(#mock_luasnip.added.python >= 3)
|
||||
|
||||
local triggers = {}
|
||||
for _, snippet in ipairs(mock_luasnip.added.python) do
|
||||
table.insert(triggers, snippet.trigger)
|
||||
end
|
||||
|
||||
assert.is_true(vim.tbl_contains(triggers, 'cp.nvim/codeforces.python'))
|
||||
assert.is_true(vim.tbl_contains(triggers, 'cp.nvim/atcoder.python'))
|
||||
assert.is_true(vim.tbl_contains(triggers, 'cp.nvim/cses.python'))
|
||||
end)
|
||||
|
||||
it('includes template content with placeholders', function()
|
||||
local config = { snippets = {} }
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
local cpp_snippets = mock_luasnip.added.cpp or {}
|
||||
local codeforces_snippet = nil
|
||||
for _, snippet in ipairs(cpp_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/codeforces.cpp' then
|
||||
codeforces_snippet = snippet
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_not_nil(codeforces_snippet)
|
||||
assert.is_not_nil(codeforces_snippet.body)
|
||||
assert.equals('table', type(codeforces_snippet.body))
|
||||
assert.is_not_nil(codeforces_snippet.body.template:match('#include'))
|
||||
assert.is_not_nil(codeforces_snippet.body.template:match('void solve'))
|
||||
end)
|
||||
|
||||
it('respects user snippet overrides', function()
|
||||
local custom_snippet = {
|
||||
trigger = 'cp.nvim/custom.cpp',
|
||||
body = 'custom template',
|
||||
}
|
||||
local config = {
|
||||
snippets = { custom_snippet },
|
||||
}
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
local cpp_snippets = mock_luasnip.added.cpp or {}
|
||||
local found_custom = false
|
||||
for _, snippet in ipairs(cpp_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/custom.cpp' then
|
||||
found_custom = true
|
||||
assert.equals('custom template', snippet.body)
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(found_custom)
|
||||
end)
|
||||
|
||||
it('filters user snippets by language', function()
|
||||
local cpp_snippet = {
|
||||
trigger = 'cp.nvim/custom.cpp',
|
||||
body = 'cpp template',
|
||||
}
|
||||
local python_snippet = {
|
||||
trigger = 'cp.nvim/custom.python',
|
||||
body = 'python template',
|
||||
}
|
||||
local config = {
|
||||
snippets = { cpp_snippet, python_snippet },
|
||||
}
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
local cpp_snippets = mock_luasnip.added.cpp or {}
|
||||
local python_snippets = mock_luasnip.added.python or {}
|
||||
|
||||
local cpp_has_custom = false
|
||||
for _, snippet in ipairs(cpp_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/custom.cpp' then
|
||||
cpp_has_custom = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local python_has_custom = false
|
||||
for _, snippet in ipairs(python_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/custom.python' then
|
||||
python_has_custom = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(cpp_has_custom)
|
||||
assert.is_true(python_has_custom)
|
||||
end)
|
||||
|
||||
it('handles empty config gracefully', function()
|
||||
assert.has_no_errors(function()
|
||||
snippets.setup({})
|
||||
end)
|
||||
|
||||
assert.is_not_nil(mock_luasnip.added.cpp)
|
||||
assert.is_not_nil(mock_luasnip.added.python)
|
||||
end)
|
||||
|
||||
it('handles empty config gracefully', function()
|
||||
assert.has_no_errors(function()
|
||||
snippets.setup({ snippets = {} })
|
||||
end)
|
||||
end)
|
||||
|
||||
it('creates templates for correct filetypes', function()
|
||||
local config = { snippets = {} }
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
assert.is_not_nil(mock_luasnip.added.cpp)
|
||||
assert.is_not_nil(mock_luasnip.added.python)
|
||||
assert.is_nil(mock_luasnip.added.c)
|
||||
assert.is_nil(mock_luasnip.added.py)
|
||||
end)
|
||||
|
||||
it('excludes overridden default snippets', function()
|
||||
local override_snippet = {
|
||||
trigger = 'cp.nvim/codeforces.cpp',
|
||||
body = 'overridden template',
|
||||
}
|
||||
local config = {
|
||||
snippets = { override_snippet },
|
||||
}
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
local cpp_snippets = mock_luasnip.added.cpp or {}
|
||||
local codeforces_count = 0
|
||||
for _, snippet in ipairs(cpp_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/codeforces.cpp' then
|
||||
codeforces_count = codeforces_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
assert.equals(1, codeforces_count)
|
||||
end)
|
||||
|
||||
it('handles case-insensitive snippet triggers', function()
|
||||
local mixed_case_snippet = {
|
||||
trigger = 'cp.nvim/CodeForces.cpp',
|
||||
body = 'mixed case template',
|
||||
}
|
||||
local upper_case_snippet = {
|
||||
trigger = 'cp.nvim/ATCODER.cpp',
|
||||
body = 'upper case template',
|
||||
}
|
||||
local config = {
|
||||
snippets = { mixed_case_snippet, upper_case_snippet },
|
||||
}
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
local cpp_snippets = mock_luasnip.added.cpp or {}
|
||||
|
||||
local has_mixed_case = false
|
||||
local has_upper_case = false
|
||||
local default_codeforces_count = 0
|
||||
local default_atcoder_count = 0
|
||||
|
||||
for _, snippet in ipairs(cpp_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/CodeForces.cpp' then
|
||||
has_mixed_case = true
|
||||
assert.equals('mixed case template', snippet.body)
|
||||
elseif snippet.trigger == 'cp.nvim/ATCODER.cpp' then
|
||||
has_upper_case = true
|
||||
assert.equals('upper case template', snippet.body)
|
||||
elseif snippet.trigger == 'cp.nvim/codeforces.cpp' then
|
||||
default_codeforces_count = default_codeforces_count + 1
|
||||
elseif snippet.trigger == 'cp.nvim/atcoder.cpp' then
|
||||
default_atcoder_count = default_atcoder_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(has_mixed_case)
|
||||
assert.is_true(has_upper_case)
|
||||
assert.equals(0, default_codeforces_count, 'Default codeforces snippet should be overridden')
|
||||
assert.equals(0, default_atcoder_count, 'Default atcoder snippet should be overridden')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
local M = {}
|
||||
|
||||
M.logged_messages = {}
|
||||
|
||||
local mock_logger = {
|
||||
log = function(msg, level)
|
||||
table.insert(M.logged_messages, { msg = msg, level = level })
|
||||
end,
|
||||
set_config = function() end,
|
||||
}
|
||||
|
||||
local function setup_vim_mocks()
|
||||
if not vim.fn then
|
||||
vim.fn = {}
|
||||
end
|
||||
vim.fn.expand = vim.fn.expand or function()
|
||||
return '/tmp/test.cpp'
|
||||
end
|
||||
vim.fn.mkdir = vim.fn.mkdir or function() end
|
||||
vim.fn.fnamemodify = vim.fn.fnamemodify or function(path)
|
||||
return path
|
||||
end
|
||||
vim.fn.tempname = vim.fn.tempname or function()
|
||||
return '/tmp/session'
|
||||
end
|
||||
if not vim.api then
|
||||
vim.api = {}
|
||||
end
|
||||
vim.api.nvim_get_current_buf = vim.api.nvim_get_current_buf or function()
|
||||
return 1
|
||||
end
|
||||
vim.api.nvim_buf_get_lines = vim.api.nvim_buf_get_lines or function()
|
||||
return { '' }
|
||||
end
|
||||
if not vim.cmd then
|
||||
vim.cmd = {}
|
||||
end
|
||||
vim.cmd = {
|
||||
only = function() end,
|
||||
e = function() end,
|
||||
split = function() end,
|
||||
vsplit = function() end,
|
||||
startinsert = function() end,
|
||||
stopinsert = function() end,
|
||||
}
|
||||
if not vim.system then
|
||||
vim.system = function(_)
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
M.logged_messages = {}
|
||||
package.loaded['cp.log'] = mock_logger
|
||||
end
|
||||
|
||||
function M.setup_full()
|
||||
M.setup()
|
||||
setup_vim_mocks()
|
||||
|
||||
local cache = require('cp.cache')
|
||||
cache.load = function() end
|
||||
cache.set_test_cases = function() end
|
||||
cache.set_file_state = function() end
|
||||
cache.get_file_state = function()
|
||||
return nil
|
||||
end
|
||||
cache.get_contest_data = function()
|
||||
return nil
|
||||
end
|
||||
cache.get_test_cases = function()
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
function M.mock_scraper_success()
|
||||
package.loaded['cp.scrape'] = {
|
||||
scrape_problem = function()
|
||||
local state = require('cp.state')
|
||||
return {
|
||||
success = true,
|
||||
problem_id = state.get_problem_id(),
|
||||
test_cases = {
|
||||
{ input = '1 2', expected = '3' },
|
||||
{ input = '3 4', expected = '7' },
|
||||
},
|
||||
test_count = 2,
|
||||
}
|
||||
end,
|
||||
scrape_contest_metadata = function(_, _)
|
||||
return {
|
||||
success = true,
|
||||
problems = {
|
||||
{ id = 'a' },
|
||||
{ id = 'b' },
|
||||
{ id = 'c' },
|
||||
},
|
||||
}
|
||||
end,
|
||||
scrape_problems_parallel = function()
|
||||
return {}
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function M.mock_async_scraper_success()
|
||||
package.loaded['cp.async.scraper'] = {
|
||||
scrape_contest_metadata_async = function(_, _, callback)
|
||||
vim.schedule(function()
|
||||
callback({
|
||||
success = true,
|
||||
problems = {
|
||||
{ id = 'a' },
|
||||
{ id = 'b' },
|
||||
{ id = 'c' },
|
||||
},
|
||||
})
|
||||
end)
|
||||
end,
|
||||
scrape_problem_async = function(_, _, problem_id, callback)
|
||||
vim.schedule(function()
|
||||
callback({
|
||||
success = true,
|
||||
problem_id = problem_id,
|
||||
test_cases = {
|
||||
{ input = '1 2', expected = '3' },
|
||||
{ input = '3 4', expected = '7' },
|
||||
},
|
||||
test_count = 2,
|
||||
timeout_ms = 2000,
|
||||
memory_mb = 256.0,
|
||||
url = 'https://example.com',
|
||||
})
|
||||
end)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function M.mock_async_scraper_failure()
|
||||
package.loaded['cp.async.scraper'] = {
|
||||
scrape_contest_metadata_async = function(_, _, callback)
|
||||
vim.schedule(function()
|
||||
callback({
|
||||
success = false,
|
||||
error = 'mock network error',
|
||||
})
|
||||
end)
|
||||
end,
|
||||
scrape_problem_async = function(_, _, problem_id, callback)
|
||||
vim.schedule(function()
|
||||
callback({
|
||||
success = false,
|
||||
problem_id = problem_id,
|
||||
error = 'mock scraping failed',
|
||||
})
|
||||
end)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function M.has_error_logged()
|
||||
for _, log_entry in ipairs(M.logged_messages) do
|
||||
if log_entry.level == vim.log.levels.ERROR then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function M.find_logged_message(pattern)
|
||||
for _, log_entry in ipairs(M.logged_messages) do
|
||||
if log_entry.msg and log_entry.msg:match(pattern) then
|
||||
return log_entry
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.fresh_require(module_name, additional_clears)
|
||||
additional_clears = additional_clears or {}
|
||||
|
||||
for _, clear_module in ipairs(additional_clears) do
|
||||
package.loaded[clear_module] = nil
|
||||
end
|
||||
package.loaded[module_name] = nil
|
||||
|
||||
return require(module_name)
|
||||
end
|
||||
|
||||
function M.teardown()
|
||||
package.loaded['cp.log'] = nil
|
||||
package.loaded['cp.scrape'] = nil
|
||||
package.loaded['cp.async.scraper'] = nil
|
||||
package.loaded['cp.async.jobs'] = nil
|
||||
package.loaded['cp.async.setup'] = nil
|
||||
package.loaded['cp.async'] = nil
|
||||
M.logged_messages = {}
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
describe('cp.telescope', function()
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
|
||||
package.preload['telescope'] = function()
|
||||
return {
|
||||
register_extension = function(ext_config)
|
||||
return ext_config
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
package.preload['telescope.pickers'] = function()
|
||||
return {
|
||||
new = function(_, _)
|
||||
return {
|
||||
find = function() end,
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
package.preload['telescope.finders'] = function()
|
||||
return {
|
||||
new_table = function(opts)
|
||||
return opts
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
package.preload['telescope.config'] = function()
|
||||
return {
|
||||
values = {
|
||||
generic_sorter = function()
|
||||
return {}
|
||||
end,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
package.preload['telescope.actions'] = function()
|
||||
return {
|
||||
select_default = {
|
||||
replace = function() end,
|
||||
},
|
||||
close = function() end,
|
||||
}
|
||||
end
|
||||
|
||||
package.preload['telescope.actions.state'] = function()
|
||||
return {
|
||||
get_selected_entry = function()
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec_helper.teardown()
|
||||
end)
|
||||
|
||||
describe('module loading', function()
|
||||
it('registers telescope extension without error', function()
|
||||
assert.has_no_errors(function()
|
||||
require('cp.pickers.telescope')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('returns module with picker function', function()
|
||||
local telescope_cp = require('cp.pickers.telescope')
|
||||
assert.is_table(telescope_cp)
|
||||
assert.is_function(telescope_cp.pick)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('basic running', function()
|
||||
it('can run and open the picker with :CP pick', function()
|
||||
local cp = require('cp')
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command({ fargs = { 'pick' } })
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue