From 21407be376e8ea80f81d869cb36a779aeab60013 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 19 Sep 2025 13:19:22 -0400 Subject: [PATCH] feat(test): rest of test suite --- spec/diff_spec.lua | 159 +++++++++++++++++++++++++++++---- spec/highlight_spec.lua | 183 +++++++++++++++++++++++++++++++++----- spec/test_render_spec.lua | 154 ++++++++++++++++++++++++++++---- 3 files changed, 439 insertions(+), 57 deletions(-) diff --git a/spec/diff_spec.lua b/spec/diff_spec.lua index bdfda29..d5b7dd4 100644 --- a/spec/diff_spec.lua +++ b/spec/diff_spec.lua @@ -2,41 +2,164 @@ describe('cp.diff', function() local diff = require('cp.diff') describe('get_available_backends', function() - it('returns vim and git backends') + it('returns vim and git backends', function() + local backends = diff.get_available_backends() + assert.same({'vim', 'git'}, backends) + end) end) describe('get_backend', function() - it('returns vim backend by name') - it('returns git backend by name') - it('returns nil for invalid name') + 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 nil for invalid name', function() + local backend = diff.get_backend('invalid') + assert.is_nil(backend) + end) end) describe('is_git_available', function() - it('returns true when git command succeeds') - it('returns false when git command fails') + it('returns true when git command succeeds', function() + local mock_system = stub(vim, 'system') + mock_system.returns({ code = 0 }) + + local result = diff.is_git_available() + assert.is_true(result) + + mock_system:revert() + end) + + it('returns false when git command fails', function() + local mock_system = stub(vim, 'system') + mock_system.returns({ code = 1 }) + + local result = diff.is_git_available() + assert.is_false(result) + + mock_system:revert() + end) end) describe('get_best_backend', function() - it('returns preferred backend when available') - it('falls back to vim when git unavailable') - it('defaults to vim backend') + it('returns preferred backend when available', function() + local mock_is_available = stub(diff, 'is_git_available') + mock_is_available.returns(true) + + local backend = diff.get_best_backend('git') + assert.equals('git', backend.name) + + mock_is_available:revert() + end) + + it('falls back to vim when git unavailable', function() + local mock_is_available = stub(diff, 'is_git_available') + mock_is_available.returns(false) + + local backend = diff.get_best_backend('git') + assert.equals('vim', backend.name) + + mock_is_available:revert() + end) + + it('defaults to vim backend', function() + local backend = diff.get_best_backend() + assert.equals('vim', backend.name) + end) end) describe('vim backend', function() - it('returns content as-is') - it('returns nil highlights') + 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('git backend', function() - it('creates temp files for diff') - it('returns raw diff output') - it('cleans up temp files') - it('handles no differences') - it('handles git command failure') + it('creates temp files for diff', function() + local mock_system = stub(vim, 'system') + local mock_tempname = stub(vim.fn, 'tempname') + local mock_writefile = stub(vim.fn, 'writefile') + local mock_delete = stub(vim.fn, 'delete') + + mock_tempname.returns('/tmp/expected', '/tmp/actual') + mock_system.returns({ code = 1, stdout = 'diff output' }) + + local backend = diff.get_backend('git') + backend.render('expected text', 'actual text') + + assert.stub(mock_writefile).was_called(2) + assert.stub(mock_delete).was_called(2) + + mock_system:revert() + mock_tempname:revert() + mock_writefile:revert() + mock_delete:revert() + end) + + it('returns raw diff output', function() + local mock_system = stub(vim, 'system') + local mock_tempname = stub(vim.fn, 'tempname') + local mock_writefile = stub(vim.fn, 'writefile') + local mock_delete = stub(vim.fn, 'delete') + + mock_tempname.returns('/tmp/expected', '/tmp/actual') + mock_system.returns({ code = 1, stdout = 'git diff output' }) + + local backend = diff.get_backend('git') + local result = backend.render('expected', 'actual') + + assert.equals('git diff output', result.raw_diff) + + mock_system:revert() + mock_tempname:revert() + mock_writefile:revert() + mock_delete:revert() + end) + + it('handles no differences', function() + local mock_system = stub(vim, 'system') + local mock_tempname = stub(vim.fn, 'tempname') + local mock_writefile = stub(vim.fn, 'writefile') + local mock_delete = stub(vim.fn, 'delete') + + mock_tempname.returns('/tmp/expected', '/tmp/actual') + mock_system.returns({ code = 0 }) + + local backend = diff.get_backend('git') + local result = backend.render('same', 'same') + + assert.same({'same'}, result.content) + assert.same({}, result.highlights) + + mock_system:revert() + mock_tempname:revert() + mock_writefile:revert() + mock_delete:revert() + end) end) describe('render_diff', function() - it('uses best available backend') - it('passes parameters to backend') + it('uses best available backend', function() + local mock_get_best = spy.on(diff, 'get_best_backend') + local mock_backend = { render = function() return {} end } + mock_get_best.returns(mock_backend) + + diff.render_diff('expected', 'actual', 'vim') + + assert.spy(mock_get_best).was_called_with('vim') + mock_get_best:revert() + end) end) end) diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 5df7dd1..ec14d43 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -1,39 +1,174 @@ describe('cp.highlight', function() local highlight = require('cp.highlight') - describe('parse_diff_line', function() - it('parses added text markers {+text+}') - it('removes removed text markers [-text-]') - it('handles mixed add/remove markers') - it('calculates correct highlight positions') - it('handles text without markers') - it('handles empty text') - end) - describe('parse_git_diff', function() - it('skips git diff headers') - it('processes added lines') - it('ignores removed lines') - it('handles unchanged lines') - it('sets correct line numbers') - it('handles empty diff output') + 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('clears existing highlights') - it('applies extmarks with correct positions') - it('uses correct highlight groups') - it('handles empty highlights') + it('clears existing highlights', function() + local mock_clear = spy.on(vim.api, 'nvim_buf_clear_namespace') + local bufnr = 1 + local namespace = 100 + + highlight.apply_highlights(bufnr, {}, namespace) + + assert.spy(mock_clear).was_called_with(bufnr, namespace, 0, -1) + mock_clear:revert() + end) + + it('applies extmarks with correct positions', function() + local mock_extmark = spy.on(vim.api, 'nvim_buf_set_extmark') + local bufnr = 1 + local namespace = 100 + local highlights = { + { + line = 0, + col_start = 5, + col_end = 10, + highlight_group = 'CpDiffAdded' + } + } + + highlight.apply_highlights(bufnr, highlights, namespace) + + assert.spy(mock_extmark).was_called_with( + bufnr, namespace, 0, 5, + { + end_col = 10, + hl_group = 'CpDiffAdded', + priority = 100 + } + ) + mock_extmark:revert() + end) + + it('uses correct highlight groups', function() + local mock_extmark = spy.on(vim.api, 'nvim_buf_set_extmark') + local highlights = { + { + line = 0, + col_start = 0, + col_end = 5, + highlight_group = 'CpDiffAdded' + } + } + + highlight.apply_highlights(1, highlights, 100) + + local call_args = mock_extmark.calls[1].vals + assert.equals('CpDiffAdded', call_args[4].hl_group) + mock_extmark:revert() + end) + + it('handles empty highlights', function() + local mock_extmark = spy.on(vim.api, 'nvim_buf_set_extmark') + + highlight.apply_highlights(1, {}, 100) + + assert.spy(mock_extmark).was_not_called() + mock_extmark:revert() + end) end) describe('create_namespace', function() - it('creates unique namespace') + it('creates unique namespace', function() + local mock_create = stub(vim.api, 'nvim_create_namespace') + mock_create.returns(42) + + local result = highlight.create_namespace() + + assert.equals(42, result) + assert.stub(mock_create).was_called_with('cp_diff_highlights') + mock_create:revert() + end) end) describe('parse_and_apply_diff', function() - it('parses diff and applies to buffer') - it('sets buffer content') - it('applies highlights') - it('returns content lines') + it('parses diff and applies to buffer', function() + local mock_set_lines = spy.on(vim.api, 'nvim_buf_set_lines') + local mock_apply = spy.on(highlight, 'apply_highlights') + local bufnr = 1 + local namespace = 100 + local diff_output = '+hello {+world+}' + + local result = highlight.parse_and_apply_diff(bufnr, diff_output, namespace) + + assert.same({'hello world'}, result) + assert.spy(mock_set_lines).was_called_with(bufnr, 0, -1, false, {'hello world'}) + assert.spy(mock_apply).was_called() + + mock_set_lines:revert() + mock_apply:revert() + end) + + it('sets buffer content', function() + local mock_set_lines = spy.on(vim.api, 'nvim_buf_set_lines') + + highlight.parse_and_apply_diff(1, '+test line', 100) + + assert.spy(mock_set_lines).was_called_with(1, 0, -1, false, {'test line'}) + mock_set_lines:revert() + end) + + it('applies highlights', function() + local mock_apply = spy.on(highlight, 'apply_highlights') + + highlight.parse_and_apply_diff(1, '+hello {+world+}', 100) + + assert.spy(mock_apply).was_called() + mock_apply:revert() + end) + + it('returns content lines', function() + local result = highlight.parse_and_apply_diff(1, '+first\n+second', 100) + assert.same({'first', 'second'}, result) + end) end) end) diff --git a/spec/test_render_spec.lua b/spec/test_render_spec.lua index 4398599..c8f7d91 100644 --- a/spec/test_render_spec.lua +++ b/spec/test_render_spec.lua @@ -2,29 +2,153 @@ describe('cp.test_render', function() local test_render = require('cp.test_render') describe('get_status_info', function() - it('returns AC for pass status') - it('returns WA for fail status with normal exit codes') - it('returns TLE for timeout status') - it('returns RTE for fail with signal codes (>= 128)') - it('returns empty for pending status') + it('returns AC for pass status', function() + local test_case = { status = 'pass' } + local result = test_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 = test_render.get_status_info(test_case) + assert.equals('WA', result.text) + assert.equals('CpTestError', result.highlight_group) + end) + + it('returns TLE for timeout status', function() + local test_case = { status = 'timeout' } + local result = test_render.get_status_info(test_case) + assert.equals('TLE', result.text) + assert.equals('CpTestError', result.highlight_group) + end) + + it('returns TLE for timed out fail status', function() + local test_case = { status = 'fail', timed_out = true } + local result = test_render.get_status_info(test_case) + assert.equals('TLE', result.text) + assert.equals('CpTestError', result.highlight_group) + end) + + it('returns RTE for fail with signal codes (>= 128)', function() + local test_case = { status = 'fail', code = 139 } + local result = test_render.get_status_info(test_case) + assert.equals('RTE', result.text) + assert.equals('CpTestError', result.highlight_group) + end) + + it('returns empty for pending status', function() + local test_case = { status = 'pending' } + local result = test_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 = test_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 test cases with CP terminology') - it('shows current test with > prefix') - it('displays input only for current test') - it('handles empty test cases') - it('preserves input line breaks') + it('renders test cases with CP terminology', function() + local test_state = { + test_cases = { + { status = 'pass', input = '5' }, + { status = 'fail', code = 1, input = '3' }, + }, + current_index = 1, + } + local result = test_render.render_test_list(test_state) + assert.equals('> 1. AC', result[1]) + assert.equals(' 2. WA', result[3]) + end) + + it('shows current test with > prefix', function() + local test_state = { + test_cases = { + { status = 'pass', input = '' }, + { status = 'pass', input = '' }, + }, + current_index = 2, + } + local result = test_render.render_test_list(test_state) + assert.equals(' 1. AC', result[1]) + assert.equals('> 2. AC', result[2]) + 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 = test_render.render_test_list(test_state) + assert.equals('> 1. AC', result[1]) + assert.equals(' 5 3', result[2]) + assert.equals(' 2. AC', result[3]) + end) + + it('handles empty test cases', function() + local test_state = { test_cases = {}, current_index = 1 } + local result = test_render.render_test_list(test_state) + assert.equals(0, #result) + end) + + it('preserves input line breaks', function() + local test_state = { + test_cases = { + { status = 'pass', input = '5\n3\n1' }, + }, + current_index = 1, + } + local result = test_render.render_test_list(test_state) + assert.equals('> 1. AC', result[1]) + assert.equals(' 5', result[2]) + assert.equals(' 3', result[3]) + assert.equals(' 1', result[4]) + end) end) describe('render_status_bar', function() - it('formats time and exit code') - it('handles missing time') - it('handles missing exit code') - it('returns empty for nil test case') + it('formats time and exit code', function() + local test_case = { time_ms = 45.7, code = 0 } + local result = test_render.render_status_bar(test_case) + assert.equals('46ms │ Exit: 0', result) + end) + + it('handles missing time', function() + local test_case = { code = 0 } + local result = test_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 = test_render.render_status_bar(test_case) + assert.equals('123ms', result) + end) + + it('returns empty for nil test case', function() + local result = test_render.render_status_bar(nil) + assert.equals('', result) + end) end) describe('setup_highlights', function() - it('sets up all highlight groups') + it('sets up all highlight groups', function() + local mock_set_hl = spy.on(vim.api, 'nvim_set_hl') + test_render.setup_highlights() + + assert.spy(mock_set_hl).was_called(5) + assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981', bold = true }) + assert.spy(mock_set_hl).was_called_with(0, 'CpTestError', { fg = '#ef4444', bold = true }) + + mock_set_hl:revert() + end) end) end)