Merge pull request #70 from barrett-ruth/feat/config-validation
Config Parameter Validation
This commit is contained in:
commit
e5dcab36c3
5 changed files with 178 additions and 51 deletions
30
doc/cp.txt
30
doc/cp.txt
|
|
@ -72,7 +72,18 @@ Here's an example configuration with lazy.nvim: >
|
||||||
'barrett-ruth/cp.nvim',
|
'barrett-ruth/cp.nvim',
|
||||||
cmd = 'CP',
|
cmd = 'CP',
|
||||||
opts = {
|
opts = {
|
||||||
contests = {},
|
contests = {
|
||||||
|
default = {
|
||||||
|
cpp = {
|
||||||
|
compile = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' },
|
||||||
|
test = { '{binary}' },
|
||||||
|
debug = { 'g++', '{source}', '-o', '{binary}', '-std=c++17', '-g', '-fsanitize=address,undefined' },
|
||||||
|
},
|
||||||
|
python = {
|
||||||
|
test = { 'python3', '{source}' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
snippets = {},
|
snippets = {},
|
||||||
hooks = {
|
hooks = {
|
||||||
before_run = nil,
|
before_run = nil,
|
||||||
|
|
@ -80,8 +91,8 @@ Here's an example configuration with lazy.nvim: >
|
||||||
setup_code = nil,
|
setup_code = nil,
|
||||||
},
|
},
|
||||||
debug = false,
|
debug = false,
|
||||||
scrapers = { atcoder = true, codeforces = true, cses = true },
|
scrapers = { ... }, -- all scrapers enabled by default
|
||||||
filename = nil,
|
filename = default_filename,
|
||||||
run_panel = {
|
run_panel = {
|
||||||
diff_mode = 'vim',
|
diff_mode = 'vim',
|
||||||
next_test_key = '<c-n>',
|
next_test_key = '<c-n>',
|
||||||
|
|
@ -91,7 +102,6 @@ Here's an example configuration with lazy.nvim: >
|
||||||
},
|
},
|
||||||
diff = {
|
diff = {
|
||||||
git = {
|
git = {
|
||||||
command = 'git',
|
|
||||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -107,14 +117,14 @@ Here's an example configuration with lazy.nvim: >
|
||||||
- {snippets} (`table[]`) LuaSnip snippet definitions.
|
- {snippets} (`table[]`) LuaSnip snippet definitions.
|
||||||
- {debug} (`boolean`, default: `false`) Show info messages
|
- {debug} (`boolean`, default: `false`) Show info messages
|
||||||
during operation.
|
during operation.
|
||||||
- {scrapers} (`table<string,boolean>`) Per-platform scraper control.
|
- {scrapers} (`table<string>`) List of enabled scrapers.
|
||||||
Default enables all platforms.
|
Default: all scrapers enabled
|
||||||
- {run_panel} (`RunPanelConfig`) Test panel behavior configuration.
|
- {run_panel} (`RunPanelConfig`) Test panel behavior configuration.
|
||||||
- {diff} (`DiffConfig`) Diff backend configuration.
|
- {diff} (`DiffConfig`) Diff backend configuration.
|
||||||
- {filename}? (`function`) Custom filename generation function.
|
- {filename}? (`function`) Custom filename generation function.
|
||||||
`function(contest, contest_id, problem_id, config, language)`
|
`function(contest, contest_id, problem_id, config, language)`
|
||||||
Should return full filename with extension.
|
Should return full filename with extension.
|
||||||
(default: concats contest_id and problem id)
|
(default: `default_filename` - concatenates contest_id and problem_id, lowercased)
|
||||||
|
|
||||||
*cp.ContestConfig*
|
*cp.ContestConfig*
|
||||||
|
|
||||||
|
|
@ -236,7 +246,7 @@ Example: Setting up and solving AtCoder contest ABC324
|
||||||
1. Browse to https://atcoder.jp/contests/abc324
|
1. Browse to https://atcoder.jp/contests/abc324
|
||||||
2. Set up contest and load metadata: >
|
2. Set up contest and load metadata: >
|
||||||
:CP atcoder abc324
|
:CP atcoder abc324
|
||||||
< This caches all problems (A, B, C, D, E, F, G) for navigation
|
< This caches all problems (A, B, ...) for navigation
|
||||||
|
|
||||||
3. Start with problem A: >
|
3. Start with problem A: >
|
||||||
:CP a
|
:CP a
|
||||||
|
|
@ -298,8 +308,8 @@ characters below) >
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
│Expected vs Actual │
|
│Expected vs Actual │
|
||||||
│4[-2-]{+3+} │
|
│4[-2-]{+3+} │
|
||||||
│ 100 │
|
│100 │
|
||||||
│ hello w[-o-]r{+o+}ld │
|
│hello w[-o-]r{+o+}ld │
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
<
|
<
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
---@field debug? string[] Debug command template
|
---@field debug? string[] Debug command template
|
||||||
---@field executable? string Executable name
|
---@field executable? string Executable name
|
||||||
---@field version? number Language version
|
---@field version? number Language version
|
||||||
---@field extension string File extension
|
---@field extension? string File extension
|
||||||
|
|
||||||
---@class PartialLanguageConfig
|
---@class PartialLanguageConfig
|
||||||
---@field compile? string[] Compile command template
|
---@field compile? string[] Compile command template
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
---@class ContestConfig
|
---@class ContestConfig
|
||||||
---@field cpp LanguageConfig
|
---@field cpp LanguageConfig
|
||||||
---@field python LanguageConfig
|
---@field python LanguageConfig
|
||||||
---@field default_language string
|
---@field default_language? string
|
||||||
|
|
||||||
---@class PartialContestConfig
|
---@class PartialContestConfig
|
||||||
---@field cpp? PartialLanguageConfig
|
---@field cpp? PartialLanguageConfig
|
||||||
|
|
@ -37,8 +37,7 @@
|
||||||
---@field max_output_lines number Maximum lines of test output to display
|
---@field max_output_lines number Maximum lines of test output to display
|
||||||
|
|
||||||
---@class DiffGitConfig
|
---@class DiffGitConfig
|
||||||
---@field command string Git executable name
|
---@field args string[] Git diff arguments
|
||||||
---@field args string[] Additional git diff arguments
|
|
||||||
|
|
||||||
---@class DiffConfig
|
---@class DiffConfig
|
||||||
---@field git DiffGitConfig
|
---@field git DiffGitConfig
|
||||||
|
|
@ -68,7 +67,26 @@ local constants = require('cp.constants')
|
||||||
|
|
||||||
---@type cp.Config
|
---@type cp.Config
|
||||||
M.defaults = {
|
M.defaults = {
|
||||||
contests = {},
|
contests = {
|
||||||
|
default = {
|
||||||
|
cpp = {
|
||||||
|
compile = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' },
|
||||||
|
test = { '{binary}' },
|
||||||
|
debug = {
|
||||||
|
'g++',
|
||||||
|
'{source}',
|
||||||
|
'-o',
|
||||||
|
'{binary}',
|
||||||
|
'-std=c++17',
|
||||||
|
'-g',
|
||||||
|
'-fsanitize=address,undefined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
python = {
|
||||||
|
test = { 'python3', '{source}' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
snippets = {},
|
snippets = {},
|
||||||
hooks = {
|
hooks = {
|
||||||
before_run = nil,
|
before_run = nil,
|
||||||
|
|
@ -87,7 +105,6 @@ M.defaults = {
|
||||||
},
|
},
|
||||||
diff = {
|
diff = {
|
||||||
git = {
|
git = {
|
||||||
command = 'git',
|
|
||||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -114,25 +131,21 @@ function M.setup(user_config)
|
||||||
|
|
||||||
if user_config.contests then
|
if user_config.contests then
|
||||||
for contest_name, contest_config in pairs(user_config.contests) do
|
for contest_name, contest_config in pairs(user_config.contests) do
|
||||||
for lang_name, lang_config in pairs(contest_config) do
|
vim.validate({
|
||||||
if type(lang_config) == 'table' and lang_config.extension then
|
[contest_name] = {
|
||||||
if
|
contest_config,
|
||||||
not vim.tbl_contains(
|
function(config)
|
||||||
vim.tbl_keys(constants.filetype_to_language),
|
if type(config) ~= 'table' then
|
||||||
lang_config.extension
|
return false
|
||||||
)
|
end
|
||||||
then
|
|
||||||
error(
|
-- Allow any language and extension configurations
|
||||||
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
|
|
||||||
lang_config.extension,
|
return true
|
||||||
lang_name,
|
end,
|
||||||
contest_name,
|
'contest configuration',
|
||||||
table.concat(vim.tbl_keys(constants.filetype_to_language), ', ')
|
},
|
||||||
)
|
})
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -226,6 +239,26 @@ function M.setup(user_config)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not contest_config.default_language then
|
||||||
|
local available_langs = {}
|
||||||
|
for lang_name, lang_config in pairs(contest_config) do
|
||||||
|
if type(lang_config) == 'table' and lang_name ~= 'default_language' then
|
||||||
|
table.insert(available_langs, lang_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #available_langs == 0 then
|
||||||
|
error('No language configurations found')
|
||||||
|
end
|
||||||
|
|
||||||
|
if vim.tbl_contains(available_langs, 'cpp') then
|
||||||
|
contest_config.default_language = 'cpp'
|
||||||
|
else
|
||||||
|
table.sort(available_langs)
|
||||||
|
contest_config.default_language = available_langs[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,17 @@ describe('cp.config', function()
|
||||||
assert.equals('table', type(result.scrapers))
|
assert.equals('table', type(result.scrapers))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('validates extension against supported filetypes', function()
|
it('allows custom extensions', function()
|
||||||
local invalid_config = {
|
local custom_config = {
|
||||||
contests = {
|
contests = {
|
||||||
test_contest = {
|
test_contest = {
|
||||||
cpp = { extension = 'invalid' },
|
cpp = { extension = 'custom' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.has_error(function()
|
assert.has_no.errors(function()
|
||||||
config.setup(invalid_config)
|
config.setup(custom_config)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -119,6 +119,101 @@ describe('cp.config', function()
|
||||||
end)
|
end)
|
||||||
end)
|
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 first available when cpp not present', 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('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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('default_filename', function()
|
describe('default_filename', function()
|
||||||
|
|
|
||||||
|
|
@ -186,18 +186,15 @@ describe('cp.execute', function()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test the internal execute_command function indirectly
|
|
||||||
local language_config = {
|
local language_config = {
|
||||||
run = { '{binary_file}' },
|
run = { '{binary_file}' },
|
||||||
}
|
}
|
||||||
|
|
||||||
-- This would be called by a higher-level function that uses execute_command
|
|
||||||
execute.compile_generic(language_config, { binary_file = './test.run' })
|
execute.compile_generic(language_config, { binary_file = './test.run' })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles command execution', function()
|
it('handles command execution', function()
|
||||||
vim.system = function(_, opts)
|
vim.system = function(_, opts)
|
||||||
-- Compilation doesn't set timeout, only text=true
|
|
||||||
if opts then
|
if opts then
|
||||||
assert.equals(true, opts.text)
|
assert.equals(true, opts.text)
|
||||||
end
|
end
|
||||||
|
|
@ -259,8 +256,6 @@ describe('cp.execute', function()
|
||||||
|
|
||||||
describe('directory creation', function()
|
describe('directory creation', function()
|
||||||
it('creates build and io directories', function()
|
it('creates build and io directories', function()
|
||||||
-- This tests the ensure_directories function indirectly
|
|
||||||
-- since it's called by other functions
|
|
||||||
local language_config = {
|
local language_config = {
|
||||||
compile = { 'mkdir', '-p', 'build', 'io' },
|
compile = { 'mkdir', '-p', 'build', 'io' },
|
||||||
}
|
}
|
||||||
|
|
@ -276,15 +271,10 @@ describe('cp.execute', function()
|
||||||
|
|
||||||
describe('language detection', function()
|
describe('language detection', function()
|
||||||
it('detects cpp from extension', function()
|
it('detects cpp from extension', function()
|
||||||
-- This tests get_language_from_file indirectly
|
|
||||||
|
|
||||||
-- Mock the file extension detection
|
|
||||||
vim.fn.fnamemodify = function()
|
vim.fn.fnamemodify = function()
|
||||||
return 'cpp'
|
return 'cpp'
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The actual function is local, but we can test it indirectly
|
|
||||||
-- through functions that use it
|
|
||||||
assert.has_no_errors(function()
|
assert.has_no_errors(function()
|
||||||
execute.compile_generic({}, {})
|
execute.compile_generic({}, {})
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ describe('cp.scrape', function()
|
||||||
stored_data = { platform = platform, contest_id = contest_id, problems = problems }
|
stored_data = { platform = platform, contest_id = contest_id, problems = problems }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Reload the scraper module to pick up the updated mock
|
|
||||||
package.loaded['cp.scrape'] = nil
|
package.loaded['cp.scrape'] = nil
|
||||||
scrape = require('cp.scrape')
|
scrape = require('cp.scrape')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue