248 lines
7.4 KiB
Lua
248 lines
7.4 KiB
Lua
describe('State module contracts', function()
|
|
local cp
|
|
local state
|
|
local logged_messages
|
|
local original_scrape_problem
|
|
local original_scrape_contest_metadata
|
|
local original_cache_get_test_cases
|
|
|
|
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
|
|
|
|
-- Mock scraping to avoid network calls
|
|
original_scrape_problem = package.loaded['cp.scrape']
|
|
package.loaded['cp.scrape'] = {
|
|
scrape_problem = function(ctx)
|
|
return {
|
|
success = true,
|
|
problem_id = ctx.problem_id,
|
|
test_cases = {
|
|
{ input = 'test input', expected = 'test output' },
|
|
},
|
|
test_count = 1,
|
|
}
|
|
end,
|
|
scrape_contest_metadata = function(platform, contest_id)
|
|
return {
|
|
success = true,
|
|
problems = {
|
|
{ id = 'a' },
|
|
{ id = 'b' },
|
|
{ id = 'c' },
|
|
},
|
|
}
|
|
end,
|
|
scrape_problems_parallel = function()
|
|
return {}
|
|
end,
|
|
}
|
|
|
|
-- Mock cache to avoid file system
|
|
local cache = require('cp.cache')
|
|
original_cache_get_test_cases = cache.get_test_cases
|
|
cache.get_test_cases = function(platform, contest_id, problem_id)
|
|
-- Return some mock test cases
|
|
return {
|
|
{ input = 'mock input', expected = 'mock output' },
|
|
}
|
|
end
|
|
|
|
-- Mock cache load/save to be no-ops
|
|
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
|
|
|
|
-- Mock vim functions that might not exist in test
|
|
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.e = function() end
|
|
vim.cmd.only = function() end
|
|
vim.cmd.split = function() end
|
|
vim.cmd.vsplit = function() end
|
|
if not vim.system then
|
|
vim.system = function(cmd)
|
|
return {
|
|
wait = function()
|
|
return { code = 0 }
|
|
end,
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Reset state completely
|
|
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
|
|
if original_scrape_problem then
|
|
package.loaded['cp.scrape'] = original_scrape_problem
|
|
end
|
|
if original_cache_get_test_cases then
|
|
local cache = require('cp.cache')
|
|
cache.get_test_cases = original_cache_get_test_cases
|
|
end
|
|
if state then
|
|
state.reset()
|
|
end
|
|
end)
|
|
|
|
it('should enforce that all modules use state getters, not direct properties', function()
|
|
local state_module = require('cp.state')
|
|
|
|
-- State module should expose getter functions
|
|
assert.equals('function', type(state_module.get_platform))
|
|
assert.equals('function', type(state_module.get_contest_id))
|
|
assert.equals('function', type(state_module.get_problem_id))
|
|
|
|
-- State module should NOT expose internal state properties directly
|
|
-- (This prevents the bug we just fixed)
|
|
assert.is_nil(state_module.platform)
|
|
assert.is_nil(state_module.contest_id)
|
|
assert.is_nil(state_module.problem_id)
|
|
end)
|
|
|
|
it('should maintain state consistency between context and direct access', function()
|
|
-- Set up a problem
|
|
cp.handle_command({ fargs = { 'codeforces', '1234', 'a' } })
|
|
|
|
-- Get context through public API
|
|
local context = cp.get_current_context()
|
|
|
|
-- Get values through state module directly
|
|
local direct_access = {
|
|
platform = state.get_platform(),
|
|
contest_id = state.get_contest_id(),
|
|
problem_id = state.get_problem_id(),
|
|
}
|
|
|
|
-- These should be identical
|
|
assert.equals(context.platform, direct_access.platform)
|
|
assert.equals(context.contest_id, direct_access.contest_id)
|
|
assert.equals(context.problem_id, direct_access.problem_id)
|
|
end)
|
|
|
|
it('should handle nil state values gracefully in all consumers', function()
|
|
-- Start with clean state (all nil)
|
|
state.reset()
|
|
|
|
-- This should NOT crash with "expected string, got nil"
|
|
assert.has_no_errors(function()
|
|
cp.handle_command({ fargs = { 'run' } })
|
|
end)
|
|
|
|
-- Should log appropriate error, not validation error
|
|
local has_validation_error = false
|
|
local has_appropriate_error = false
|
|
for _, log_entry in ipairs(logged_messages) do
|
|
if log_entry.msg:match('expected string, got nil') then
|
|
has_validation_error = true
|
|
elseif log_entry.msg:match('No contest configured') then
|
|
has_appropriate_error = true
|
|
end
|
|
end
|
|
|
|
assert.is_false(has_validation_error, 'Should not have validation errors')
|
|
assert.is_true(has_appropriate_error, 'Should have appropriate user-facing error')
|
|
end)
|
|
|
|
it('should pass state module (not state data) to runner functions', function()
|
|
-- This is the core bug we fixed - runner expects state module, not state data
|
|
local run = require('cp.runner.run')
|
|
local problem = require('cp.problem')
|
|
|
|
-- Set up proper state
|
|
state.set_platform('codeforces')
|
|
state.set_contest_id('1234')
|
|
state.set_problem_id('a')
|
|
|
|
local ctx = problem.create_context('codeforces', '1234', 'a', {
|
|
contests = { codeforces = { cpp = { extension = 'cpp' } } },
|
|
})
|
|
|
|
-- This should work - passing the state MODULE
|
|
assert.has_no_errors(function()
|
|
run.load_test_cases(ctx, state)
|
|
end)
|
|
|
|
-- This would be the bug - passing state DATA instead of state MODULE
|
|
local fake_state_data = {
|
|
platform = 'codeforces',
|
|
contest_id = '1234',
|
|
problem_id = 'a',
|
|
}
|
|
|
|
-- This should fail gracefully (function should check for get_* methods)
|
|
local success = pcall(function()
|
|
run.load_test_cases(ctx, fake_state_data)
|
|
end)
|
|
|
|
-- The current implementation would crash because fake_state_data has no get_* methods
|
|
-- This test documents the expected behavior
|
|
assert.is_false(success, 'Should fail when passed wrong state type')
|
|
end)
|
|
|
|
it('should handle state transitions correctly', function()
|
|
-- Test that state changes are reflected everywhere
|
|
|
|
-- Initial state
|
|
cp.handle_command({ fargs = { 'codeforces', '1234', 'a' } })
|
|
assert.equals('a', cp.get_current_context().problem_id)
|
|
|
|
-- Navigate to next problem
|
|
cp.handle_command({ fargs = { 'codeforces', '1234', 'b' } })
|
|
assert.equals('b', cp.get_current_context().problem_id)
|
|
|
|
-- State should be consistent everywhere
|
|
assert.equals('b', state.get_problem_id())
|
|
end)
|
|
end)
|