repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/tests/std/dbg/viewer_spec.lua
Rawlocal viewer = require 'std.dbg.viewer'
local file = require 'std.dbg.file'
local encode = require 'std.dbg.encode'
describe( 'std.dbg.viewer', function ()
local test_log_path
local original_input
before_each( function ()
package.loaded['std.dbg.viewer'] = nil
viewer = require 'std.dbg.viewer'
test_log_path = file.get_path()
file.clear_log()
vim.g.bugabinga_debug_mode = true
original_input = vim.ui.input
rawset(vim.ui, 'input', function( _, on_choice )
on_choice( nil )
end)
end )
after_each( function ()
pcall(viewer.close)
file.clear_log()
vim.g.bugabinga_debug_mode = false
rawset(vim.ui, 'input', original_input)
end )
local function write_event(event)
local f = assert(io.open(test_log_path, 'a'))
f:write(encode.encode(event) .. '\n')
f:close()
end
local function make_event(overrides)
local e = {
kind = 'DEBUG',
date = os.time(),
message = 'test message',
data = { 'test' },
filename = 'test.lua',
fn = 'test_fn',
line = 10,
}
if overrides then
for k, v in pairs(overrides) do e[k] = v end
end
return e
end
describe( 'Phase 1: lifecycle', function ()
it( 'open() creates a split window', function ()
write_event(make_event())
viewer.open()
assert.is_true(viewer.is_open())
local wins = vim.api.nvim_list_wins()
assert.is_true(#wins > 1)
end )
it( 'open() creates a scratch buffer', function ()
write_event(make_event())
viewer.open()
local buf_id = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
assert.equal('nofile', vim.bo[buf_id].buftype)
assert.equal(false, vim.bo[buf_id].buflisted)
end )
it( 'open() sets buffer filetype to DbgViewer', function ()
write_event(make_event())
viewer.open()
local buf_id = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
assert.equal('DbgViewer', vim.bo[buf_id].filetype)
end )
it( 'close() closes the window', function ()
write_event(make_event())
viewer.open()
local win_id = vim.api.nvim_get_current_win()
viewer.close()
assert.is_false(vim.api.nvim_win_is_valid(win_id))
end )
it( 'close() wipes the buffer', function ()
write_event(make_event())
viewer.open()
local buf_id = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
viewer.close()
assert.is_false(vim.api.nvim_buf_is_valid(buf_id))
end )
it( 'is_open() returns false initially', function ()
assert.is_false(viewer.is_open())
end )
it( 'is_open() returns true after open()', function ()
write_event(make_event())
viewer.open()
assert.is_true(viewer.is_open())
end )
it( 'open() reuses existing window', function ()
write_event(make_event())
viewer.open()
local first_win = vim.api.nvim_get_current_win()
viewer.open()
local second_win = vim.api.nvim_get_current_win()
assert.equal(first_win, second_win)
end )
end )
describe( 'Phase 2: rendering', function ()
it( 'render() shows kind icon in header', function ()
write_event(make_event({ kind = 'DEBUG', message = 'hello' }))
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
assert.is_not_nil(lines[1]:match 'DEBUG')
end )
it( 'render() shows relative time', function ()
write_event(make_event({ date = os.time() - 120, message = 'time test' }))
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
assert.is_not_nil(lines[1]:match 'mi')
end )
it( 'render() shows message', function ()
write_event(make_event({ message = 'my special message' }))
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local found = false
for _, l in ipairs(lines) do
if l:match 'my special message' then found = true break end
end
assert.is_true(found)
end )
it( 'render() shows source location', function ()
write_event(make_event({ filename = 'myfile.lua', line = 42, message = 'loc test' }))
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local found = false
for _, l in ipairs(lines) do
if l:match 'myfile%.lua.*42' then found = true break end
end
assert.is_true(found)
end )
it( 'render() separates events with blank lines', function ()
write_event(make_event({ message = 'ev1' }))
write_event(make_event({ message = 'ev2' }))
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local blank_count = 0
for _, l in ipairs(lines) do
if l == '' then blank_count = blank_count + 1 end
end
assert.is_true(blank_count >= 1)
end )
it( 'render() handles empty events', function ()
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
assert.is_not_nil(lines[1])
end )
it( 'render() applies kind highlight groups via namespace', function ()
write_event(make_event({ kind = 'ERROR', message = 'err test' }))
viewer.open()
local buf_id = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
local hls = vim.api.nvim_buf_get_extmarks(buf_id, -1, 0, -1, { details = true })
assert.is_true(#hls > 0)
end )
end )
describe( 'Phase 3: filtering', function ()
it( 'filter_by_kind filters events', function ()
write_event(make_event({ kind = 'DEBUG', message = 'debug msg' }))
write_event(make_event({ kind = 'ERROR', message = 'error msg' }))
viewer.open()
viewer.clear_filters()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local debug_count = 0
for _, l in ipairs(lines) do
if l:match 'DEBUG' then debug_count = debug_count + 1 end
end
local error_count = 0
for _, l in ipairs(lines) do
if l:match 'ERROR' then error_count = error_count + 1 end
end
assert.is_true(debug_count >= 1)
assert.is_true(error_count >= 1)
end )
it( 'filter_by_message filters events', function ()
write_event(make_event({ message = 'unique_debug_msg_xyz' }))
write_event(make_event({ message = 'other_msg' }))
viewer.open()
viewer.clear_filters()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local found = false
for _, l in ipairs(lines) do
if l:match 'unique_debug_msg_xyz' then found = true break end
end
assert.is_true(found)
end )
it( 'filter_by_location filters events', function ()
write_event(make_event({ filename = 'special_file.lua', message = 'file1' }))
write_event(make_event({ filename = 'other.lua', message = 'file2' }))
viewer.open()
viewer.clear_filters()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local found = false
for _, l in ipairs(lines) do
if l:match 'special_file' then found = true break end
end
assert.is_true(found)
end )
it( 'clear_filters shows all events', function ()
write_event(make_event({ message = 'msg1' }))
write_event(make_event({ message = 'msg2' }))
viewer.open()
viewer.clear_filters()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local msg1, msg2 = false, false
for _, l in ipairs(lines) do
if l:match 'msg1' then msg1 = true end
if l:match 'msg2' then msg2 = true end
end
assert.is_true(msg1 and msg2)
end )
it( 'multiple filters combine with AND', function ()
write_event(make_event({ kind = 'ERROR', message = 'err1', filename = 'a.lua' }))
write_event(make_event({ kind = 'ERROR', message = 'err2', filename = 'b.lua' }))
write_event(make_event({ kind = 'DEBUG', message = 'dbg1', filename = 'a.lua' }))
viewer.open()
viewer.clear_filters()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local err_count = 0
for _, l in ipairs(lines) do
if l:match 'ERROR' then err_count = err_count + 1 end
end
assert.is_true(err_count >= 2)
end )
end )
describe( 'Phase 4: keymaps', function ()
it( 'q closes viewer', function ()
write_event(make_event())
viewer.open()
local win_id = vim.api.nvim_get_current_win()
vim.api.nvim_win_call(win_id, function()
vim.cmd 'normal q'
end)
assert.is_false(viewer.is_open())
end )
it( '<Esc> closes viewer', function ()
write_event(make_event())
viewer.open()
viewer.close()
assert.is_false(viewer.is_open())
end )
it( 'r refreshes events from disk', function ()
write_event(make_event({ message = 'original' }))
viewer.open()
write_event(make_event({ message = 'newly_added' }))
local win_id = vim.api.nvim_get_current_win()
vim.api.nvim_win_call(win_id, function()
vim.cmd 'normal r'
end)
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(win_id), 0, -1, false)
local found = false
for _, l in ipairs(lines) do
if l:match 'newly_added' then found = true break end
end
assert.is_true(found)
end )
it( '<CR> jumps to source', function ()
local jump_path = vim.fn.fnamemodify(vim.fn.getcwd(), ':p') .. 'test_jump_target.lua'
local f = assert(io.open(jump_path, 'w'))
f:write 'local x = 1\nlocal y = 2\n'
f:close()
write_event(make_event({ filename = jump_path, line = 2, message = 'jump test' }))
viewer.open()
local viewer_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(viewer_win, { 2, 0 })
local curbuf_before = vim.api.nvim_get_current_buf()
vim.api.nvim_win_call(viewer_win, function()
viewer.jump_to_source(viewer_win)
end)
local curbuf_after = vim.api.nvim_get_current_buf()
local name = vim.api.nvim_buf_get_name(curbuf_after)
assert(name ~= nil, 'buffer should have a name after jump')
assert(name:match 'test_jump_target' ~= nil, 'should jump to test file: ' .. tostring(name))
vim.fn.delete(jump_path)
end )
it( 'ff filters combined (all)', function ()
write_event(make_event({ kind = 'DEBUG', message = 'foo' }))
write_event(make_event({ kind = 'ERROR', message = 'bar' }))
viewer.open()
viewer.clear_filters()
local lines_before = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local foo_before = false
for _, l in ipairs(lines_before) do
if l:match 'foo' then foo_before = true break end
end
assert.is_true(foo_before)
local win_id = vim.api.nvim_get_current_win()
vim.api.nvim_win_call(win_id, function()
vim.cmd 'normal ff'
end)
assert.is_true(viewer.is_open())
end )
it( 'fc clears filters and notifies', function ()
write_event(make_event({ message = 'fc_test' }))
viewer.open()
local win_id = vim.api.nvim_get_current_win()
vim.api.nvim_win_call(win_id, function()
vim.cmd 'normal fc'
end)
assert.is_true(viewer.is_open())
end )
end )
describe( 'Phase 5: edge cases', function ()
it( 'handles events with missing fields gracefully', function ()
local f = assert(io.open(test_log_path, 'a'))
f:write(vim.json.encode({ kind = 'DEBUG', message = 'partial' }) .. '\n')
f:close()
viewer.open()
assert.is_true(viewer.is_open())
end )
it( 'handles very long messages', function ()
local long_msg = string.rep('x', 1000)
write_event(make_event({ message = long_msg }))
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
assert.is_true(#lines > 0)
end )
it( 'handles many events without blocking', function ()
for i = 1, 50 do
write_event(make_event({ message = 'event ' .. i }))
end
local ok = pcall(function()
viewer.open()
end)
assert.is_true(ok)
assert.is_true(viewer.is_open())
end )
it( 'jump_to_source jumps to correct file after auto-refresh prepend', function ()
local path1 = vim.fn.fnamemodify(vim.fn.getcwd(), ':p') .. 'jump_test_1.lua'
local path2 = vim.fn.fnamemodify(vim.fn.getcwd(), ':p') .. 'jump_test_2.lua'
local path3 = vim.fn.fnamemodify(vim.fn.getcwd(), ':p') .. 'jump_test_3.lua'
local path4 = vim.fn.fnamemodify(vim.fn.getcwd(), ':p') .. 'jump_test_4.lua'
for _, p in ipairs({ path1, path2, path3, path4 }) do
local f = assert(io.open(p, 'w'))
f:write('line1\nline2\nline3\nline4\nline5\n')
f:close()
end
write_event(make_event({ filename = path1, line = 2, message = 'event1' }))
write_event(make_event({ filename = path2, line = 3, message = 'event2' }))
viewer.open()
write_event(make_event({ filename = path3, line = 4, message = 'event3' }))
write_event(make_event({ filename = path4, line = 5, message = 'event4' }))
viewer._on_file_change()
local viewer_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(viewer_win, { 1, 0 })
vim.api.nvim_win_call(viewer_win, function()
viewer.jump_to_source(viewer_win)
end)
local curbuf = vim.api.nvim_get_current_buf()
local name = vim.api.nvim_buf_get_name(curbuf)
assert(name:match 'jump_test_4' ~= nil, 'should jump to newest event (path4), got: ' .. tostring(name))
for _, p in ipairs({ path1, path2, path3, path4 }) do
vim.fn.delete(p)
end
end )
it( 'watcher remains alive after auto-refresh', function ()
write_event(make_event({ message = 'e1' }))
viewer.open()
assert(assert(viewer._get_state()).watcher, 'watcher should exist after open')
write_event(make_event({ message = 'e2' }))
viewer._on_file_change()
assert(assert(viewer._get_state()).watcher, 'watcher should still exist after file change')
end )
it( 'original event highlights survive auto-refresh', function ()
write_event(make_event({ kind = 'ERROR', message = 'event1' }))
write_event(make_event({ kind = 'DEBUG', message = 'event2' }))
viewer.open()
local buf_id = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
local extmarks_before = vim.api.nvim_buf_get_extmarks(buf_id, -1, 0, -1, { details = true })
assert(#extmarks_before > 0, 'should have highlights on initial render')
write_event(make_event({ kind = 'TRACE', message = 'event3' }))
viewer._on_file_change()
local extmarks_after = vim.api.nvim_buf_get_extmarks(buf_id, -1, 0, -1, { details = true })
assert(#extmarks_after >= #extmarks_before, 'should retain original highlights after prepend')
end )
it( 'sanitizes [string "..."] source to unknown', function ()
local chunk_event = {
kind = 'DEBUG',
date = os.time(),
message = 'chunk test',
data = {},
filename = '[string "chunk:123"]',
fn = 'test_fn',
line = 10,
}
write_event(chunk_event)
viewer.open()
local lines = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), 0, -1, false)
local found_unknown = false
for _, l in ipairs(lines) do
if l:match 'unknown' then found_unknown = true break end
end
assert(found_unknown, 'should show "unknown" for [string "..."] source')
end )
it( 'filtered_indices stay consistent after auto-refresh', function ()
write_event(make_event({ kind = 'DEBUG', message = 'debug1' }))
viewer.open()
viewer.clear_filters()
local state_before = assert(viewer._get_state())
local filtered_count_before = state_before.filtered_count
write_event(make_event({ kind = 'DEBUG', message = 'debug2' }))
viewer._on_file_change()
local state_after = assert(viewer._get_state())
assert.equal(filtered_count_before + 1, state_after.filtered_count, 'filtered_count should increment for new matching event')
end )
end )
end )