Merge pull request #48 from barrett-ruth/feat/lua-testing
Mature Lua Testing
This commit is contained in:
commit
0c3f62d1e0
17 changed files with 1765 additions and 86 deletions
9
.github/workflows/luarocks.yml
vendored
9
.github/workflows/luarocks.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Push to Luarocks
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -7,11 +7,12 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
luarocks-upload:
|
||||
runs-on: ubuntu-22.04
|
||||
publish-luarocks:
|
||||
name: Publish to LuaRocks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: LuaRocks Upload
|
||||
- name: Publish to LuaRocks
|
||||
uses: nvim-neorocks/luarocks-tag-release@v7
|
||||
env:
|
||||
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
name: ci
|
||||
name: Code Quality
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
@ -7,9 +7,38 @@ on:
|
|||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lua-format:
|
||||
name: Lua Formatting
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
lua: ${{ steps.changes.outputs.lua }}
|
||||
python: ${{ steps.changes.outputs.python }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
lua:
|
||||
- 'lua/**'
|
||||
- 'spec/**'
|
||||
- 'plugin/**'
|
||||
- 'after/**'
|
||||
- 'ftdetect/**'
|
||||
- '*.lua'
|
||||
- '.luarc.json'
|
||||
- 'stylua.toml'
|
||||
- 'selene.toml'
|
||||
python:
|
||||
- 'scrapers/**'
|
||||
- 'tests/scrapers/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
lua-format:
|
||||
name: Lua Format Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: JohnnyMorganz/stylua-action@v4
|
||||
|
|
@ -19,8 +48,10 @@ jobs:
|
|||
args: --check .
|
||||
|
||||
lua-lint:
|
||||
name: Lua Linting
|
||||
name: Lua Lint Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint with Selene
|
||||
|
|
@ -30,8 +61,10 @@ jobs:
|
|||
args: --display-style quiet .
|
||||
|
||||
lua-typecheck:
|
||||
name: Lua Type Checking
|
||||
name: Lua Type Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Lua LS Type Check
|
||||
|
|
@ -42,8 +75,10 @@ jobs:
|
|||
configpath: .luarc.json
|
||||
|
||||
python-format:
|
||||
name: Python Formatting
|
||||
name: Python Format Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
|
|
@ -54,8 +89,10 @@ jobs:
|
|||
run: ruff format --check scrapers/ tests/scrapers/
|
||||
|
||||
python-lint:
|
||||
name: Python Linting
|
||||
name: Python Lint Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
|
|
@ -66,8 +103,10 @@ jobs:
|
|||
run: ruff check scrapers/ tests/scrapers/
|
||||
|
||||
python-typecheck:
|
||||
name: Python Type Checking
|
||||
name: Python Type Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
|
|
@ -75,16 +114,4 @@ jobs:
|
|||
- name: Install dependencies with mypy
|
||||
run: uv sync --dev
|
||||
- name: Type check Python files with mypy
|
||||
run: uv run mypy scrapers/ tests/scrapers/
|
||||
|
||||
python-test:
|
||||
name: Python Testing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies with pytest
|
||||
run: uv sync --dev
|
||||
- name: Run Python tests
|
||||
run: uv run pytest tests/scrapers/ -v
|
||||
run: uv run mypy scrapers/ tests/scrapers/
|
||||
65
.github/workflows/test.yml
vendored
65
.github/workflows/test.yml
vendored
|
|
@ -1,21 +1,64 @@
|
|||
name: Run tests
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request: ~
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Run tests
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
neovim_version: ['nightly', 'stable']
|
||||
|
||||
outputs:
|
||||
lua: ${{ steps.changes.outputs.lua }}
|
||||
python: ${{ steps.changes.outputs.python }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
lua:
|
||||
- 'lua/**'
|
||||
- 'spec/**'
|
||||
- 'plugin/**'
|
||||
- 'after/**'
|
||||
- 'ftdetect/**'
|
||||
- '*.lua'
|
||||
- '.luarc.json'
|
||||
- 'stylua.toml'
|
||||
- 'selene.toml'
|
||||
python:
|
||||
- 'scrapers/**'
|
||||
- 'tests/scrapers/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
lua-test:
|
||||
name: Lua Tests (${{ matrix.neovim_version }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
strategy:
|
||||
matrix:
|
||||
neovim_version: ['stable', 'nightly']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Lua tests
|
||||
uses: nvim-neorocks/nvim-busted-action@v1
|
||||
with:
|
||||
nvim_version: ${{ matrix.neovim_version }}
|
||||
|
||||
python-test:
|
||||
name: Python Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies with pytest
|
||||
run: uv sync --dev
|
||||
- name: Run Python tests
|
||||
run: uv run pytest tests/scrapers/ -v
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
|||
doc/tags
|
||||
*.log
|
||||
build
|
||||
io
|
||||
debug
|
||||
venv/
|
||||
CLAUDE.md
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ follows:
|
|||
|
||||
## TODO
|
||||
|
||||
- general `:CP test` window improvements
|
||||
- fzf/telescope integration (whichever available)
|
||||
- autocomplete with --lang and --debug
|
||||
- finer-tuned problem limits (i.e. per-problem codeforces time, memory)
|
||||
- notify discord members
|
||||
|
|
|
|||
54
doc/cp.txt
54
doc/cp.txt
|
|
@ -3,7 +3,7 @@
|
|||
Author: Barrett Ruth <br.barrettruth@gmail.com>
|
||||
License: Same terms as Vim itself (see |license|)
|
||||
|
||||
INTRODUCTION *cp* *cp.nvim*
|
||||
INTRODUCTION *cp* *cp.nvim*
|
||||
|
||||
cp.nvim is a competitive programming plugin that automates problem setup,
|
||||
compilation, and testing workflow for online judges.
|
||||
|
|
@ -11,7 +11,7 @@ compilation, and testing workflow for online judges.
|
|||
Supported platforms: AtCoder, Codeforces, CSES
|
||||
Supported languages: C++, Python
|
||||
|
||||
REQUIREMENTS *cp-requirements*
|
||||
REQUIREMENTS *cp-requirements*
|
||||
|
||||
- Neovim 0.10.0+
|
||||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
|
|
@ -20,9 +20,9 @@ REQUIREMENTS *cp-requirements*
|
|||
Optional:
|
||||
- LuaSnip for template expansion (https://github.com/L3MON4D3/LuaSnip)
|
||||
|
||||
COMMANDS *cp-commands*
|
||||
COMMANDS *cp-commands*
|
||||
|
||||
*:CP*
|
||||
*:CP*
|
||||
cp.nvim uses a single :CP command with intelligent argument parsing:
|
||||
|
||||
Setup Commands ~
|
||||
|
|
@ -63,7 +63,7 @@ Navigation Commands ~
|
|||
:CP prev Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
|
||||
CONFIGURATION *cp-config*
|
||||
CONFIGURATION *cp-config*
|
||||
|
||||
cp.nvim works out of the box. No setup required.
|
||||
|
||||
|
|
@ -166,16 +166,16 @@ Optional configuration with lazy.nvim: >
|
|||
Used to configure buffer settings.
|
||||
`function(ctx: ProblemContext)`
|
||||
|
||||
WORKFLOW *cp-workflow*
|
||||
WORKFLOW *cp-workflow*
|
||||
|
||||
For the sake of consistency and simplicity, cp.nvim extracts contest/problem identifiers from
|
||||
URLs. This means that, for example, CodeForces/AtCoder contests are configured by
|
||||
their round id rather than round number. See below.
|
||||
|
||||
PLATFORM-SPECIFIC USAGE *cp-platforms*
|
||||
PLATFORM-SPECIFIC USAGE *cp-platforms*
|
||||
|
||||
AtCoder ~
|
||||
*cp-atcoder*
|
||||
*cp-atcoder*
|
||||
URL format: https://atcoder.jp/contests/abc123/tasks/abc123_a
|
||||
|
||||
In terms of cp.nvim, this corresponds to:
|
||||
|
|
@ -190,7 +190,7 @@ Usage examples: >
|
|||
:CP next " Navigate to next problem in contest
|
||||
<
|
||||
Codeforces ~
|
||||
*cp-codeforces*
|
||||
*cp-codeforces*
|
||||
URL format: https://codeforces.com/contest/1234/problem/A
|
||||
|
||||
In terms of cp.nvim, this corresponds to:
|
||||
|
|
@ -205,7 +205,7 @@ Usage examples: >
|
|||
:CP prev " Navigate to previous problem in contest
|
||||
<
|
||||
CSES ~
|
||||
*cp-cses*
|
||||
*cp-cses*
|
||||
URL format: https://cses.fi/problemset/task/1068
|
||||
|
||||
CSES is organized by categories rather than contests. Currently all problems
|
||||
|
|
@ -221,7 +221,7 @@ Usage examples: >
|
|||
:CP 1070 " Switch to problem 1070 (if CSES loaded)
|
||||
:CP next " Navigate to next problem in CSES
|
||||
<
|
||||
COMPLETE WORKFLOW EXAMPLE *cp-example*
|
||||
COMPLETE WORKFLOW EXAMPLE *cp-example*
|
||||
|
||||
Example: Setting up and solving AtCoder contest ABC324
|
||||
|
||||
|
|
@ -254,13 +254,13 @@ Example: Quick setup for single Codeforces problem >
|
|||
:CP test " Test immediately
|
||||
<
|
||||
|
||||
TEST PANEL *cp-test*
|
||||
TEST PANEL *cp-test*
|
||||
|
||||
The test panel provides individual test case debugging with a three-pane
|
||||
layout showing test list, expected output, and actual output side-by-side.
|
||||
|
||||
Activation ~
|
||||
*:CP-test*
|
||||
*:CP-test*
|
||||
:CP test [--debug] Toggle test panel on/off. When activated,
|
||||
replaces current layout with test interface.
|
||||
Automatically compiles and runs all tests.
|
||||
|
|
@ -272,24 +272,24 @@ Interface ~
|
|||
|
||||
The test panel uses a three-pane layout for easy comparison: >
|
||||
|
||||
┌─ Test List ─────────────────────────────────────────────────┐
|
||||
│ 1. PASS 12ms │
|
||||
│> 2. FAIL 45ms │
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. [ok:true ] [code:0] [time:12ms] │
|
||||
│> 2. [ok:false] [code:0] [time:45ms] │
|
||||
│ │
|
||||
│ ── Input ── │
|
||||
│ Input: │
|
||||
│ 5 3 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─ Expected ──────────────┐ ┌─ Actual ────────────────┐
|
||||
│ 8 │ │ 7 │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────────────┘ └─────────────────────────┘
|
||||
┌─ Expected ──────────────────┐ ┌───── Actual ────────────────┐
|
||||
│ 8 │ │ 7 │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────────────────┘ └─────────────────────────────┘
|
||||
<
|
||||
|
||||
Keymaps ~
|
||||
*cp-test-keys*
|
||||
*cp-test-keys*
|
||||
j / <Down> Navigate to next test case
|
||||
k / <Up> Navigate to previous test case
|
||||
q Exit test panel (restore layout)
|
||||
|
|
@ -301,7 +301,7 @@ execution pipeline, but with isolated input/output for
|
|||
precise failure analysis. All tests are automatically run when the
|
||||
panel opens.
|
||||
|
||||
FILE STRUCTURE *cp-files*
|
||||
FILE STRUCTURE *cp-files*
|
||||
|
||||
cp.nvim creates the following file structure upon problem setup:
|
||||
|
||||
|
|
@ -316,7 +316,7 @@ cp.nvim creates the following file structure upon problem setup:
|
|||
The plugin automatically manages this structure and navigation between problems
|
||||
maintains proper file associations.
|
||||
|
||||
SNIPPETS *cp-snippets*
|
||||
SNIPPETS *cp-snippets*
|
||||
|
||||
cp.nvim integrates with LuaSnip for automatic template expansion. Built-in
|
||||
snippets include basic C++ and Python templates for each contest type.
|
||||
|
|
@ -326,7 +326,7 @@ CodeForces, "cses" for CSES, etc.).
|
|||
|
||||
Custom snippets can be added via the `snippets` configuration field.
|
||||
|
||||
HEALTH CHECK *cp-health*
|
||||
HEALTH CHECK *cp-health*
|
||||
|
||||
Run |:checkhealth| cp to verify your setup.
|
||||
|
||||
|
|
|
|||
|
|
@ -126,25 +126,36 @@ function M.setup(user_config)
|
|||
end
|
||||
|
||||
if user_config.scrapers then
|
||||
for contest_name, enabled in pairs(user_config.scrapers) do
|
||||
if not vim.tbl_contains(constants.PLATFORMS, contest_name) then
|
||||
for _, platform_name in ipairs(user_config.scrapers) do
|
||||
if type(platform_name) ~= 'string' then
|
||||
error(('Invalid scraper value type. Expected string, got %s'):format(type(platform_name)))
|
||||
end
|
||||
if not vim.tbl_contains(constants.PLATFORMS, platform_name) then
|
||||
error(
|
||||
("Invalid contest '%s' in scrapers config. Valid contests: %s"):format(
|
||||
contest_name,
|
||||
("Invalid platform '%s' in scrapers config. Valid platforms: %s"):format(
|
||||
platform_name,
|
||||
table.concat(constants.PLATFORMS, ', ')
|
||||
)
|
||||
)
|
||||
end
|
||||
if type(enabled) ~= 'boolean' then
|
||||
error(
|
||||
("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled))
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
|
||||
|
||||
for _, contest_config in pairs(config.contests) do
|
||||
for lang_name, lang_config in pairs(contest_config) do
|
||||
if type(lang_config) == 'table' and not lang_config.extension then
|
||||
if lang_name == 'cpp' then
|
||||
lang_config.extension = 'cpp'
|
||||
elseif lang_name == 'python' then
|
||||
lang_config.extension = 'py'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,7 @@ function M.create_context(contest, contest_id, problem_id, config, language)
|
|||
|
||||
local base_name
|
||||
if config.filename then
|
||||
local source_file = config.filename(contest, contest_id, problem_id, config, language)
|
||||
base_name = vim.fn.fnamemodify(source_file, ':t:r')
|
||||
base_name = config.filename(contest, contest_id, problem_id, config, language)
|
||||
else
|
||||
local default_filename = require('cp.config').default_filename
|
||||
base_name = default_filename(contest_id, problem_id)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ if vim.g.loaded_cp then
|
|||
end
|
||||
vim.g.loaded_cp = 1
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
||||
vim.api.nvim_create_user_command('CP', function(opts)
|
||||
local cp = require('cp')
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -14,6 +10,10 @@ end, {
|
|||
nargs = '*',
|
||||
desc = 'Competitive programming helper',
|
||||
complete = 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
|
||||
|
|
|
|||
108
spec/cache_spec.lua
Normal file
108
spec/cache_spec.lua
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
describe('cp.cache', function()
|
||||
local cache
|
||||
|
||||
before_each(function()
|
||||
cache = require('cp.cache')
|
||||
cache.load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
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)
|
||||
assert.is_not_nil(result.expires_at)
|
||||
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)
|
||||
end)
|
||||
276
spec/command_parsing_spec.lua
Normal file
276
spec/command_parsing_spec.lua
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
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
|
||||
|
||||
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
|
||||
end)
|
||||
|
||||
describe('empty arguments', function()
|
||||
it('logs error 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('Usage:') 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 = { 'test' } }
|
||||
|
||||
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', '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 = { 'test', '--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 = { 'test', '--debug' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles combined language and debug flags', function()
|
||||
local opts = { fargs = { 'test', '--lang=cpp', '--debug' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
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', 'test', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles multiple language flags', function()
|
||||
local opts = { fargs = { 'test', '--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)
|
||||
end)
|
||||
89
spec/config_spec.lua
Normal file
89
spec/config_spec.lua
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
describe('cp.config', function()
|
||||
local config
|
||||
|
||||
before_each(function()
|
||||
config = require('cp.config')
|
||||
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('validates extension against supported filetypes', function()
|
||||
local invalid_config = {
|
||||
contests = {
|
||||
test_contest = {
|
||||
cpp = { extension = 'invalid' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_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 hook functions', function()
|
||||
local invalid_config = {
|
||||
hooks = { before_run = 'not_a_function' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
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)
|
||||
346
spec/execute_spec.lua
Normal file
346
spec/execute_spec.lua
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
describe('cp.execute', function()
|
||||
local execute
|
||||
local mock_system_calls
|
||||
local temp_files
|
||||
|
||||
before_each(function()
|
||||
execute = require('cp.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
|
||||
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('g++', compile_call.cmd[1])
|
||||
assert.equals('test.cpp', compile_call.cmd[2])
|
||||
assert.equals('-o', compile_call.cmd[3])
|
||||
assert.equals('test.run', compile_call.cmd[4])
|
||||
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.equals('-omain.out', compile_call.cmd[3])
|
||||
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('g++', compile_call.cmd[1])
|
||||
assert.is_true(vim.tbl_contains(compile_call.cmd, '-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
|
||||
|
||||
-- Test the internal execute_command function indirectly
|
||||
local language_config = {
|
||||
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' })
|
||||
end)
|
||||
|
||||
it('handles command execution', function()
|
||||
vim.system = function(_, opts)
|
||||
-- Compilation doesn't set timeout, only text=true
|
||||
if opts then
|
||||
assert.equals(true, 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('parameter validation', function()
|
||||
it('validates language_config parameter', function()
|
||||
assert.has_error(function()
|
||||
execute.compile_generic(nil, {})
|
||||
end)
|
||||
|
||||
assert.has_error(function()
|
||||
execute.compile_generic('not_table', {})
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates substitutions parameter', function()
|
||||
assert.has_error(function()
|
||||
execute.compile_generic({}, nil)
|
||||
end)
|
||||
|
||||
assert.has_error(function()
|
||||
execute.compile_generic({}, 'not_table')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('directory creation', 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 = {
|
||||
compile = { 'mkdir', '-p', 'build', 'io' },
|
||||
}
|
||||
|
||||
execute.compile_generic(language_config, {})
|
||||
|
||||
local mkdir_call = mock_system_calls[1]
|
||||
assert.equals('mkdir', mkdir_call.cmd[1])
|
||||
assert.is_true(vim.tbl_contains(mkdir_call.cmd, 'build'))
|
||||
assert.is_true(vim.tbl_contains(mkdir_call.cmd, 'io'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('language detection', function()
|
||||
it('detects cpp from extension', function()
|
||||
-- This tests get_language_from_file indirectly
|
||||
|
||||
-- Mock the file extension detection
|
||||
vim.fn.fnamemodify = function()
|
||||
return 'cpp'
|
||||
end
|
||||
|
||||
-- The actual function is local, but we can test it indirectly
|
||||
-- through functions that use it
|
||||
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('echo', echo_call.cmd[1])
|
||||
assert.equals('hello', echo_call.cmd[2])
|
||||
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('g++g++', call.cmd[1])
|
||||
assert.equals('test.cpptest.cpp', call.cmd[2])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
local cp = require('cp')
|
||||
|
||||
describe('neovim plugin', function()
|
||||
it('work as expect', function()
|
||||
cp.setup()
|
||||
end)
|
||||
end)
|
||||
140
spec/problem_spec.lua
Normal file
140
spec/problem_spec.lua
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
describe('cp.problem', function()
|
||||
local problem
|
||||
|
||||
before_each(function()
|
||||
problem = require('cp.problem')
|
||||
end)
|
||||
|
||||
describe('create_context', function()
|
||||
local base_config = {
|
||||
contests = {
|
||||
atcoder = {
|
||||
default_language = 'cpp',
|
||||
cpp = { extension = 'cpp' },
|
||||
python = { extension = 'py' },
|
||||
},
|
||||
codeforces = {
|
||||
default_language = 'cpp',
|
||||
cpp = { extension = 'cpp' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
it('creates basic context with required fields', function()
|
||||
local context = problem.create_context('atcoder', 'abc123', 'a', base_config)
|
||||
|
||||
assert.equals('atcoder', context.contest)
|
||||
assert.equals('abc123', context.contest_id)
|
||||
assert.equals('a', context.problem_id)
|
||||
assert.equals('abc123a', context.problem_name)
|
||||
assert.equals('abc123a.cpp', context.source_file)
|
||||
assert.equals('build/abc123a.run', context.binary_file)
|
||||
assert.equals('io/abc123a.cpin', context.input_file)
|
||||
assert.equals('io/abc123a.cpout', context.output_file)
|
||||
assert.equals('io/abc123a.expected', context.expected_file)
|
||||
end)
|
||||
|
||||
it('handles context without problem_id', function()
|
||||
local context = problem.create_context('codeforces', '1933', nil, base_config)
|
||||
|
||||
assert.equals('codeforces', context.contest)
|
||||
assert.equals('1933', context.contest_id)
|
||||
assert.is_nil(context.problem_id)
|
||||
assert.equals('1933', context.problem_name)
|
||||
assert.equals('1933.cpp', context.source_file)
|
||||
assert.equals('build/1933.run', context.binary_file)
|
||||
end)
|
||||
|
||||
it('uses default language from contest config', function()
|
||||
local context = problem.create_context('atcoder', 'abc123', 'a', base_config)
|
||||
assert.equals('abc123a.cpp', context.source_file)
|
||||
end)
|
||||
|
||||
it('respects explicit language parameter', function()
|
||||
local context = problem.create_context('atcoder', 'abc123', 'a', base_config, 'python')
|
||||
assert.equals('abc123a.py', context.source_file)
|
||||
end)
|
||||
|
||||
it('uses custom filename function when provided', function()
|
||||
local config_with_custom = vim.tbl_deep_extend('force', base_config, {
|
||||
filename = function(contest, contest_id, problem_id)
|
||||
return contest .. '_' .. contest_id .. (problem_id and ('_' .. problem_id) or '')
|
||||
end,
|
||||
})
|
||||
|
||||
local context = problem.create_context('atcoder', 'abc123', 'a', config_with_custom)
|
||||
assert.equals('atcoder_abc123_a.cpp', context.source_file)
|
||||
assert.equals('atcoder_abc123_a', context.problem_name)
|
||||
end)
|
||||
|
||||
it('validates required parameters', function()
|
||||
assert.has_error(function()
|
||||
problem.create_context(nil, 'abc123', 'a', base_config)
|
||||
end)
|
||||
|
||||
assert.has_error(function()
|
||||
problem.create_context('atcoder', nil, 'a', base_config)
|
||||
end)
|
||||
|
||||
assert.has_error(function()
|
||||
problem.create_context('atcoder', 'abc123', 'a', nil)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates contest exists in config', function()
|
||||
assert.has_error(function()
|
||||
problem.create_context('invalid_contest', 'abc123', 'a', base_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates language exists in contest config', function()
|
||||
assert.has_error(function()
|
||||
problem.create_context('atcoder', 'abc123', 'a', base_config, 'invalid_language')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates default language exists', function()
|
||||
local bad_config = {
|
||||
contests = {
|
||||
test_contest = {
|
||||
default_language = 'nonexistent',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
problem.create_context('test_contest', 'abc123', 'a', bad_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates language extension is configured', function()
|
||||
local bad_config = {
|
||||
contests = {
|
||||
test_contest = {
|
||||
default_language = 'cpp',
|
||||
cpp = {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
problem.create_context('test_contest', 'abc123', 'a', bad_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles complex contest and problem ids', function()
|
||||
local context = problem.create_context('atcoder', 'arc123', 'f', base_config)
|
||||
assert.equals('arc123f', context.problem_name)
|
||||
assert.equals('arc123f.cpp', context.source_file)
|
||||
assert.equals('build/arc123f.run', context.binary_file)
|
||||
end)
|
||||
|
||||
it('generates correct io file paths', function()
|
||||
local context = problem.create_context('atcoder', 'abc123', 'a', base_config)
|
||||
|
||||
assert.equals('io/abc123a.cpin', context.input_file)
|
||||
assert.equals('io/abc123a.cpout', context.output_file)
|
||||
assert.equals('io/abc123a.expected', context.expected_file)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
430
spec/scraper_spec.lua
Normal file
430
spec/scraper_spec.lua
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
describe('cp.scrape', function()
|
||||
local scrape
|
||||
local mock_cache
|
||||
local mock_system_calls
|
||||
local temp_files
|
||||
|
||||
before_each(function()
|
||||
temp_files = {}
|
||||
|
||||
mock_cache = {
|
||||
load = function() end,
|
||||
get_contest_data = function()
|
||||
return nil
|
||||
end,
|
||||
set_contest_data = function() end,
|
||||
}
|
||||
|
||||
mock_system_calls = {}
|
||||
|
||||
vim.system = function(cmd, opts)
|
||||
table.insert(mock_system_calls, { cmd = cmd, opts = opts })
|
||||
|
||||
local result = { code = 0, stdout = '{}', stderr = '' }
|
||||
|
||||
if cmd[1] == 'ping' then
|
||||
result = { code = 0 }
|
||||
elseif cmd[1] == 'uv' and cmd[2] == 'sync' then
|
||||
result = { code = 0, stdout = '', stderr = '' }
|
||||
elseif cmd[1] == 'uv' and cmd[2] == 'run' then
|
||||
if vim.tbl_contains(cmd, 'metadata') then
|
||||
result.stdout = '{"success": true, "problems": [{"id": "a", "name": "Test Problem"}]}'
|
||||
elseif vim.tbl_contains(cmd, 'tests') then
|
||||
result.stdout =
|
||||
'{"success": true, "tests": [{"input": "1 2", "expected": "3"}], "url": "https://example.com"}'
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
wait = function()
|
||||
return result
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
package.loaded['cp.cache'] = mock_cache
|
||||
scrape = require('cp.scrape')
|
||||
|
||||
local original_fn = vim.fn
|
||||
vim.fn = vim.tbl_extend('force', vim.fn, {
|
||||
executable = function(cmd)
|
||||
if cmd == 'uv' then
|
||||
return 1
|
||||
end
|
||||
return original_fn.executable(cmd)
|
||||
end,
|
||||
isdirectory = function(path)
|
||||
if path:match('%.venv$') then
|
||||
return 1
|
||||
end
|
||||
return original_fn.isdirectory(path)
|
||||
end,
|
||||
filereadable = function(path)
|
||||
if temp_files[path] then
|
||||
return 1
|
||||
end
|
||||
return 0
|
||||
end,
|
||||
readfile = function(path)
|
||||
return temp_files[path] or {}
|
||||
end,
|
||||
writefile = function(lines, path)
|
||||
temp_files[path] = lines
|
||||
end,
|
||||
mkdir = function() end,
|
||||
fnamemodify = function(path, modifier)
|
||||
if modifier == ':r' then
|
||||
return path:gsub('%..*$', '')
|
||||
end
|
||||
return original_fn.fnamemodify(path, modifier)
|
||||
end,
|
||||
})
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
package.loaded['cp.cache'] = nil
|
||||
vim.system = vim.system_original or vim.system
|
||||
temp_files = {}
|
||||
end)
|
||||
|
||||
describe('cache integration', function()
|
||||
it('returns cached data when available', function()
|
||||
mock_cache.get_contest_data = function(platform, contest_id)
|
||||
if platform == 'atcoder' and contest_id == 'abc123' then
|
||||
return { problems = { { id = 'a', name = 'Cached Problem' } } }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_true(result.success)
|
||||
assert.equals(1, #result.problems)
|
||||
assert.equals('Cached Problem', result.problems[1].name)
|
||||
assert.equals(0, #mock_system_calls)
|
||||
end)
|
||||
|
||||
it('stores scraped data in cache after successful scrape', function()
|
||||
local stored_data = nil
|
||||
mock_cache.set_contest_data = function(platform, contest_id, problems)
|
||||
stored_data = { platform = platform, contest_id = contest_id, problems = problems }
|
||||
end
|
||||
|
||||
-- Reload the scraper module to pick up the updated mock
|
||||
package.loaded['cp.scrape'] = nil
|
||||
scrape = require('cp.scrape')
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_true(result.success)
|
||||
assert.is_not_nil(stored_data)
|
||||
assert.equals('atcoder', stored_data.platform)
|
||||
assert.equals('abc123', stored_data.contest_id)
|
||||
assert.equals(1, #stored_data.problems)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('system dependency checks', function()
|
||||
it('handles missing uv executable', function()
|
||||
vim.fn.executable = function(cmd)
|
||||
return cmd == 'uv' and 0 or 1
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_false(result.success)
|
||||
assert.is_not_nil(result.error:match('Python environment setup failed'))
|
||||
end)
|
||||
|
||||
it('handles python environment setup failure', function()
|
||||
vim.system = function(cmd)
|
||||
if cmd[1] == 'ping' then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
elseif cmd[1] == 'uv' and cmd[2] == 'sync' then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stderr = 'setup failed' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
vim.fn.isdirectory = function()
|
||||
return 0
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_false(result.success)
|
||||
assert.is_not_nil(result.error:match('Python environment setup failed'))
|
||||
end)
|
||||
|
||||
it('handles network connectivity issues', function()
|
||||
vim.system = function(cmd)
|
||||
if cmd[1] == 'ping' then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_false(result.success)
|
||||
assert.equals('No internet connection available', result.error)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('subprocess execution', function()
|
||||
it('constructs correct command for atcoder metadata', function()
|
||||
scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
local metadata_call = nil
|
||||
for _, call in ipairs(mock_system_calls) do
|
||||
if vim.tbl_contains(call.cmd, 'metadata') then
|
||||
metadata_call = call
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_not_nil(metadata_call)
|
||||
assert.equals('uv', metadata_call.cmd[1])
|
||||
assert.equals('run', metadata_call.cmd[2])
|
||||
assert.is_true(vim.tbl_contains(metadata_call.cmd, 'metadata'))
|
||||
assert.is_true(vim.tbl_contains(metadata_call.cmd, 'abc123'))
|
||||
end)
|
||||
|
||||
it('constructs correct command for cses metadata', function()
|
||||
scrape.scrape_contest_metadata('cses', 'problemset')
|
||||
|
||||
local metadata_call = nil
|
||||
for _, call in ipairs(mock_system_calls) do
|
||||
if vim.tbl_contains(call.cmd, 'metadata') then
|
||||
metadata_call = call
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_not_nil(metadata_call)
|
||||
assert.equals('uv', metadata_call.cmd[1])
|
||||
assert.is_true(vim.tbl_contains(metadata_call.cmd, 'metadata'))
|
||||
assert.is_false(vim.tbl_contains(metadata_call.cmd, 'problemset'))
|
||||
end)
|
||||
|
||||
it('handles subprocess execution failure', function()
|
||||
vim.system = function(cmd)
|
||||
if cmd[1] == 'ping' then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
elseif cmd[1] == 'uv' and vim.tbl_contains(cmd, 'metadata') then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stderr = 'execution failed' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_false(result.success)
|
||||
assert.is_not_nil(result.error:match('Failed to run metadata scraper'))
|
||||
assert.is_not_nil(result.error:match('execution failed'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('json parsing', function()
|
||||
it('handles invalid json output', function()
|
||||
vim.system = function(cmd)
|
||||
if cmd[1] == 'ping' then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
elseif cmd[1] == 'uv' and vim.tbl_contains(cmd, 'metadata') then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = 'invalid json' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_false(result.success)
|
||||
assert.is_not_nil(result.error:match('Failed to parse metadata scraper output'))
|
||||
end)
|
||||
|
||||
it('handles scraper-reported failures', function()
|
||||
vim.system = function(cmd)
|
||||
if cmd[1] == 'ping' then
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
elseif cmd[1] == 'uv' and vim.tbl_contains(cmd, 'metadata') then
|
||||
return {
|
||||
wait = function()
|
||||
return {
|
||||
code = 0,
|
||||
stdout = '{"success": false, "error": "contest not found"}',
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local result = scrape.scrape_contest_metadata('atcoder', 'abc123')
|
||||
|
||||
assert.is_false(result.success)
|
||||
assert.equals('contest not found', result.error)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('problem scraping', function()
|
||||
local test_context
|
||||
|
||||
before_each(function()
|
||||
test_context = {
|
||||
contest = 'atcoder',
|
||||
contest_id = 'abc123',
|
||||
problem_id = 'a',
|
||||
problem_name = 'abc123a',
|
||||
input_file = 'io/abc123a.cpin',
|
||||
expected_file = 'io/abc123a.expected',
|
||||
}
|
||||
end)
|
||||
|
||||
it('uses existing files when available', function()
|
||||
temp_files['io/abc123a.cpin'] = { '1 2' }
|
||||
temp_files['io/abc123a.expected'] = { '3' }
|
||||
temp_files['io/abc123a.1.cpin'] = { '4 5' }
|
||||
temp_files['io/abc123a.1.cpout'] = { '9' }
|
||||
|
||||
local result = scrape.scrape_problem(test_context)
|
||||
|
||||
assert.is_true(result.success)
|
||||
assert.equals('abc123a', result.problem_id)
|
||||
assert.equals(1, result.test_count)
|
||||
assert.equals(0, #mock_system_calls)
|
||||
end)
|
||||
|
||||
it('scrapes and writes test case files', function()
|
||||
local result = scrape.scrape_problem(test_context)
|
||||
|
||||
assert.is_true(result.success)
|
||||
assert.equals('abc123a', result.problem_id)
|
||||
assert.equals(1, result.test_count)
|
||||
assert.is_not_nil(temp_files['io/abc123a.1.cpin'])
|
||||
assert.is_not_nil(temp_files['io/abc123a.1.cpout'])
|
||||
assert.equals('1 2', table.concat(temp_files['io/abc123a.1.cpin'], '\n'))
|
||||
assert.equals('3', table.concat(temp_files['io/abc123a.1.cpout'], '\n'))
|
||||
end)
|
||||
|
||||
it('constructs correct command for atcoder problem tests', function()
|
||||
scrape.scrape_problem(test_context)
|
||||
|
||||
local tests_call = nil
|
||||
for _, call in ipairs(mock_system_calls) do
|
||||
if vim.tbl_contains(call.cmd, 'tests') then
|
||||
tests_call = call
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_not_nil(tests_call)
|
||||
assert.is_true(vim.tbl_contains(tests_call.cmd, 'tests'))
|
||||
assert.is_true(vim.tbl_contains(tests_call.cmd, 'abc123'))
|
||||
assert.is_true(vim.tbl_contains(tests_call.cmd, 'a'))
|
||||
end)
|
||||
|
||||
it('constructs correct command for cses problem tests', function()
|
||||
test_context.contest = 'cses'
|
||||
test_context.contest_id = '1001'
|
||||
test_context.problem_id = nil
|
||||
|
||||
scrape.scrape_problem(test_context)
|
||||
|
||||
local tests_call = nil
|
||||
for _, call in ipairs(mock_system_calls) do
|
||||
if vim.tbl_contains(call.cmd, 'tests') then
|
||||
tests_call = call
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_not_nil(tests_call)
|
||||
assert.is_true(vim.tbl_contains(tests_call.cmd, 'tests'))
|
||||
assert.is_true(vim.tbl_contains(tests_call.cmd, '1001'))
|
||||
assert.is_false(vim.tbl_contains(tests_call.cmd, 'a'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('error scenarios', function()
|
||||
it('validates input parameters', function()
|
||||
assert.has_error(function()
|
||||
scrape.scrape_contest_metadata(nil, 'abc123')
|
||||
end)
|
||||
|
||||
assert.has_error(function()
|
||||
scrape.scrape_contest_metadata('atcoder', nil)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('handles file system errors gracefully', function()
|
||||
vim.fn.mkdir = function()
|
||||
error('permission denied')
|
||||
end
|
||||
|
||||
local ctx = {
|
||||
contest = 'atcoder',
|
||||
contest_id = 'abc123',
|
||||
problem_id = 'a',
|
||||
problem_name = 'abc123a',
|
||||
input_file = 'io/abc123a.cpin',
|
||||
expected_file = 'io/abc123a.expected',
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
scrape.scrape_problem(ctx)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
215
spec/snippets_spec.lua
Normal file
215
spec/snippets_spec.lua
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
describe('cp.snippets', function()
|
||||
local snippets
|
||||
local mock_luasnip
|
||||
|
||||
before_each(function()
|
||||
snippets = 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()
|
||||
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)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue