diff --git a/.luacheckrc b/.luacheckrc index 1889f10..e501ee2 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -57,4 +57,9 @@ files = { "122", -- Setting read-only field vim.notify (intentional mocking) }, }, -} \ No newline at end of file + ["tests/test_error_handling.lua"] = { + ignore = { + "122", -- Setting read-only fields for intentional mocking in tests + }, + }, +} diff --git a/tests/test_edge_cases.lua b/tests/test_edge_cases.lua new file mode 100644 index 0000000..c9f19dc --- /dev/null +++ b/tests/test_edge_cases.lua @@ -0,0 +1,417 @@ +-- Edge case tests +local T = MiniTest.new_set() +local helpers = require("tests.helpers") + +-- Initialize plugin at file load time +require("code-review").setup({ + comment = { + storage = { backend = "memory" }, + }, +}) + +-- Setup and teardown +T.hooks = { + pre_case = function() + -- Reset and reinitialize for clean state + local state = require("code-review.state") + local memory = require("code-review.storage.memory") + + -- Use _reset for complete cleanup + state._reset() + memory._reset() + + -- Reinitialize + state.init() + + -- Clear any existing comments + state.clear() + end, +} + +-- Large data tests +T["large data"] = MiniTest.new_set() + +T["large data"]["handles very large comments"] = function() + -- Create a very large comment (10KB) + local large_comment = string.rep("This is a long comment line. ", 350) + local state = require("code-review.state") + + local id = state.add_comment({ + file = "test.lua", + line_start = 1, + line_end = 1, + comment = large_comment, + }) + + MiniTest.expect.equality(type(id), "string") + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.comment, large_comment) +end + +T["large data"]["handles many comments"] = function() + local state = require("code-review.state") + + -- Get initial comment count + local initial_comments = state.get_comments() + + -- Add 100 comments + local ids = {} + for i = 1, 100 do + local id = state.add_comment({ + file = string.format("file%d.lua", i), + line_start = i, + line_end = i, + comment = string.format("Comment number %d", i), + }) + table.insert(ids, id) + end + + -- Verify all comments exist + local all_comments = state.get_comments() + local added_count = #all_comments - #initial_comments + MiniTest.expect.equality(added_count, 100) + + -- Verify we can retrieve specific comments + local comment50 = state.get_comment(ids[50]) + helpers.expect.match(comment50.comment, "Comment number 50") +end + +-- Special characters tests +T["special characters"] = MiniTest.new_set() + +T["special characters"]["handles quotes and escapes in comments"] = function() + local state = require("code-review.state") + + local special_comments = { + [[This has "double quotes"]], + [[This has 'single quotes']], + [[This has `backticks`]], + [[This has \backslashes\]], + [[This has +newlines +in it]], + [[This has tabs in it]], + } + + for i, comment_text in ipairs(special_comments) do + local id = state.add_comment({ + file = "special_chars_test.lua", -- Use different file name + line_start = i, + line_end = i, + comment = comment_text, + }) + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.comment, comment_text) + end +end + +T["special characters"]["handles unicode characters"] = function() + local state = require("code-review.state") + + local unicode_comments = { + "This has emojis 🚀 ✨ 🎉", + "これは日本語のコメントです", + "这是中文评论", + "Это русский комментарий", + "هذا تعليق عربي", + } + + for i, comment_text in ipairs(unicode_comments) do + local id = state.add_comment({ + file = "unicode_test.lua", -- Use different file name + line_start = i, + line_end = i, + comment = comment_text, + }) + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.comment, comment_text) + end +end + +T["special characters"]["handles special file names"] = function() + local state = require("code-review.state") + + local special_files = { + "file with spaces.lua", + "file-with-dashes.lua", + "file_with_underscores.lua", + "file.with.dots.lua", + "файл.lua", -- Cyrillic + "文件.lua", -- Chinese + "path/to/nested/file.lua", + "/absolute/path/to/file.lua", + "~/home/path/file.lua", + } + + for _, filename in ipairs(special_files) do + local id = state.add_comment({ + file = filename, + line_start = 1, + line_end = 1, + comment = "Test comment", + }) + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.file, filename) + end +end + +-- Boundary tests +T["boundary conditions"] = MiniTest.new_set() + +T["boundary conditions"]["handles empty comment"] = function() + local state = require("code-review.state") + + local id = state.add_comment({ + file = "test.lua", + line_start = 1, + line_end = 1, + comment = "", + }) + + MiniTest.expect.equality(type(id), "string") + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.comment, "") +end + +T["boundary conditions"]["handles whitespace-only comment"] = function() + local state = require("code-review.state") + + local whitespace_comments = { + " ", + " ", + "\t", + "\n", + "\n\n\n", + " \t\n ", + } + + for i, comment_text in ipairs(whitespace_comments) do + local id = state.add_comment({ + file = "whitespace_test.lua", -- Use different file name + line_start = i, + line_end = i, + comment = comment_text, + }) + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.comment, comment_text) + end +end + +T["boundary conditions"]["handles single-line file"] = function() + local state = require("code-review.state") + + -- Create buffer with single line + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "single line" }) + vim.api.nvim_buf_set_name(buf, "single.lua") + + local id = state.add_comment({ + file = "single.lua", + line_start = 1, + line_end = 1, + comment = "Comment on single line", + }) + + MiniTest.expect.equality(type(id), "string") + + -- Cleanup + pcall(vim.api.nvim_buf_delete, buf, { force = true }) +end + +T["boundary conditions"]["handles comment spanning entire file"] = function() + local state = require("code-review.state") + + -- Create buffer with multiple lines + local buf = vim.api.nvim_create_buf(false, true) + local lines = {} + for i = 1, 100 do + table.insert(lines, string.format("line %d", i)) + end + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_buf_set_name(buf, "large.lua") + + local id = state.add_comment({ + file = "large.lua", + line_start = 1, + line_end = 100, + comment = "Comment spanning entire file", + context_lines = lines, + }) + + MiniTest.expect.equality(type(id), "string") + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.line_start, 1) + MiniTest.expect.equality(retrieved.line_end, 100) + MiniTest.expect.equality(#retrieved.context_lines, 100) + + -- Cleanup + pcall(vim.api.nvim_buf_delete, buf, { force = true }) +end + +-- Multiple comments tests +T["multiple comments"] = MiniTest.new_set() + +T["multiple comments"]["handles multiple comments on same line"] = function() + local state = require("code-review.state") + + -- Add multiple comments on the same line + local ids = {} + for i = 1, 5 do + local id = state.add_comment({ + file = "test.lua", + line_start = 10, + line_end = 10, + comment = string.format("Comment %d on line 10", i), + }) + table.insert(ids, id) + end + + -- Verify all comments exist + local comments_at_line = state.get_comments_at_location("test.lua", 10) + MiniTest.expect.equality(#comments_at_line, 5) + + -- Verify each comment + for i = 1, 5 do + local found = false + for _, comment in ipairs(comments_at_line) do + if comment.comment == string.format("Comment %d on line 10", i) then + found = true + break + end + end + MiniTest.expect.equality(found, true) + end +end + +T["multiple comments"]["handles overlapping comment ranges"] = function() + local state = require("code-review.state") + + -- Add overlapping comments + state.add_comment({ + file = "test.lua", + line_start = 5, + line_end = 10, + comment = "Comment 1: lines 5-10", + }) + + state.add_comment({ + file = "test.lua", + line_start = 8, + line_end = 15, + comment = "Comment 2: lines 8-15", + }) + + state.add_comment({ + file = "test.lua", + line_start = 7, + line_end = 12, + comment = "Comment 3: lines 7-12", + }) + + -- Check comments at overlapping lines + local comments_at_8 = state.get_comments_at_location("test.lua", 8) + MiniTest.expect.equality(#comments_at_8, 3) -- All three comments include line 8 + + local comments_at_5 = state.get_comments_at_location("test.lua", 5) + MiniTest.expect.equality(#comments_at_5, 1) -- Only comment 1 + + local comments_at_15 = state.get_comments_at_location("test.lua", 15) + MiniTest.expect.equality(#comments_at_15, 1) -- Only comment 2 +end + +-- File handling tests +T["file handling"] = MiniTest.new_set() + +T["file handling"]["handles files without extensions"] = function() + local state = require("code-review.state") + + local files_without_ext = { + "Makefile", + "Dockerfile", + "LICENSE", + "README", + ".gitignore", + ".env", + } + + for _, filename in ipairs(files_without_ext) do + local id = state.add_comment({ + file = filename, + line_start = 1, + line_end = 1, + comment = "Comment on " .. filename, + }) + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.file, filename) + end +end + +T["file handling"]["handles very long file paths"] = function() + local state = require("code-review.state") + + -- Create a very long path (300+ chars) + local long_path = "/very/long/path/" .. string.rep("subdir/", 40) .. "file.lua" + + local id = state.add_comment({ + file = long_path, + line_start = 1, + line_end = 1, + comment = "Comment on file with long path", + }) + + local retrieved = state.get_comment(id) + MiniTest.expect.equality(retrieved.file, long_path) + MiniTest.expect.equality(#retrieved.file > 300, true) +end + +-- Performance tests +T["performance"] = MiniTest.new_set() + +T["performance"]["handles rapid add/delete operations"] = function() + local state = require("code-review.state") + + -- Get initial count + local initial_count = #state.get_comments() + + local start_time = vim.loop.hrtime() + + -- Track added IDs + local added_ids = {} + + -- Rapidly add and delete comments + for i = 1, 50 do + local id = state.add_comment({ + file = "test.lua", + line_start = i, + line_end = i, + comment = "Rapid comment " .. i, + }) + + if i % 2 == 0 then + -- Delete every other comment immediately + state.delete_comment(id) + else + table.insert(added_ids, id) + end + end + + local elapsed = (vim.loop.hrtime() - start_time) / 1e6 -- Convert to milliseconds + + -- Should complete in reasonable time (less than 1 second) + MiniTest.expect.equality(elapsed < 1000, true) + + -- Verify final state + local final_comments = state.get_comments() + local added_count = #final_comments - initial_count + MiniTest.expect.equality(added_count, 25) -- Half should remain +end + +return T diff --git a/tests/test_error_handling.lua b/tests/test_error_handling.lua new file mode 100644 index 0000000..83cb04d --- /dev/null +++ b/tests/test_error_handling.lua @@ -0,0 +1,252 @@ +-- Error handling tests +local T = MiniTest.new_set() +local helpers = require("tests.helpers") + +-- Track notify messages +local notify_messages = {} + +-- Store original functions +local original_notify = vim.notify +local original_setreg = vim.fn.setreg +local original_sign_place = vim.fn.sign_place +local original_sign_unplace = vim.fn.sign_unplace +local original_io_open = io.open + +-- Initialize plugin at file load time +require("code-review").setup({ + comment = { + storage = { backend = "memory" }, + }, +}) + +-- Setup and teardown +T.hooks = { + pre_case = function() + -- Reset and reinitialize for clean state + local state = require("code-review.state") + local memory = require("code-review.storage.memory") + + -- Use _reset for complete cleanup + state._reset() + memory._reset() + + -- Reinitialize + state.init() + + -- Mock vim.notify to capture messages + notify_messages = {} + vim.notify = function(msg, level) + table.insert(notify_messages, { msg = msg, level = level }) + end + end, + + post_case = function() + -- Restore original functions + vim.notify = original_notify + vim.fn.setreg = original_setreg + vim.fn.sign_place = original_sign_place + vim.fn.sign_unplace = original_sign_unplace + io.open = original_io_open + end, +} + +-- File I/O errors +T["file I/O errors"] = MiniTest.new_set() + +T["file I/O errors"]["save_to_file handles write failure"] = function() + -- Use existing directory to avoid mkdir + local test_dir = vim.fn.tempname() + vim.fn.mkdir(test_dir, "p") + local test_path = test_dir .. "/test_save.txt" + + -- Mock io.open to fail + io.open = function(path, mode) + if path == test_path then + return nil, "Permission denied" + end + return original_io_open(path, mode) + end + + local utils = require("code-review.utils") + local success = utils.save_to_file(test_path, "content") + + MiniTest.expect.equality(success, false) + -- The error message is printed outside our mock scope, so just check success is false + + -- Cleanup + vim.fn.delete(test_dir, "rf") +end + +-- UI errors +T["ui errors"] = MiniTest.new_set() + +T["ui errors"]["handles buffer creation failure"] = function() + -- Store original + local original_nvim_create_buf = vim.api.nvim_create_buf + + -- Mock to fail + vim.api.nvim_create_buf = function() + error("Buffer creation failed") + end + + local ui = require("code-review.ui") + local ok, err = pcall(ui.show_comment_input, function() end) + + MiniTest.expect.equality(ok, false) + helpers.expect.match(err, "Buffer creation failed") + + -- Restore + vim.api.nvim_create_buf = original_nvim_create_buf +end + +T["ui errors"]["handles window creation failure"] = function() + -- Store originals + local original_nvim_open_win = vim.api.nvim_open_win + local original_nvim_create_buf = vim.api.nvim_create_buf + + -- Mock buffer creation to succeed + local test_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_create_buf = function() + return test_buf + end + + -- Mock window creation to fail + vim.api.nvim_open_win = function() + error("Window creation failed") + end + + local ui = require("code-review.ui") + local ok, err = pcall(ui.show_comment_input, function() end) + + MiniTest.expect.equality(ok, false) + helpers.expect.match(err, "Window creation failed") + + -- Cleanup + pcall(vim.api.nvim_buf_delete, test_buf, { force = true }) + + -- Restore + vim.api.nvim_open_win = original_nvim_open_win + vim.api.nvim_create_buf = original_nvim_create_buf +end + +T["ui errors"]["handles invalid window operations"] = function() + -- Store original + local original_nvim_win_is_valid = vim.api.nvim_win_is_valid + + -- Mock to return false + vim.api.nvim_win_is_valid = function() + return false + end + + local ui = require("code-review.ui") + + -- Try to show comment list with invalid window + local ok = pcall(ui.show_comment_list, {}) + + -- Should handle gracefully (not crash) + MiniTest.expect.equality(type(ok), "boolean") + + -- Restore + vim.api.nvim_win_is_valid = original_nvim_win_is_valid +end + +-- Boundary and validation +T["boundary and validation"] = MiniTest.new_set() + +T["boundary and validation"]["handles nil input gracefully"] = function() + local state = require("code-review.state") + + -- Try to add comment with empty values + local ok = pcall(state.add_comment, { + file = "", + line_start = 1, -- Use valid line numbers + line_end = 1, + comment = "", + }) + + -- Should handle gracefully + MiniTest.expect.equality(ok, true) + + -- Verify it was added + local comments = state.get_comments() + local found = false + for _, c in ipairs(comments) do + if c.file == "" and c.comment == "" then + found = true + break + end + end + MiniTest.expect.equality(found, true) +end + +T["boundary and validation"]["handles invalid line numbers"] = function() + local state = require("code-review.state") + + -- Try with reversed line numbers + local id = state.add_comment({ + file = "test.lua", + line_start = 10, + line_end = 5, -- End before start + comment = "Reversed line numbers", + }) + + -- Should still create comment + MiniTest.expect.equality(type(id), "string") + + local comment = state.get_comment(id) + MiniTest.expect.equality(comment.line_start, 10) + MiniTest.expect.equality(comment.line_end, 5) +end + +T["boundary and validation"]["handles empty state operations"] = function() + local state = require("code-review.state") + + -- Clear all comments + state.clear() + + -- Operations on empty state + local comments = state.get_comments() + MiniTest.expect.equality(#comments, 0) + + local location_comments = state.get_comments_at_location("any.lua", 1) + MiniTest.expect.equality(#location_comments, 0) + + local non_existent = state.get_comment("non-existent-id") + MiniTest.expect.equality(non_existent, nil) + + local delete_result = state.delete_comment("non-existent-id") + MiniTest.expect.equality(delete_result, false) +end + +-- Optional dependency handling +T["optional dependencies"] = MiniTest.new_set() + +T["optional dependencies"]["handles missing telescope gracefully"] = function() + -- Mock telescope to not exist + package.loaded["telescope"] = nil + package.loaded["telescope.builtin"] = nil + + local comment = require("code-review.comment") + + -- Try to select comment which uses telescope if available + local ok = pcall(comment.select_comment) + + -- Should handle gracefully + MiniTest.expect.equality(type(ok), "boolean") +end + +T["optional dependencies"]["handles missing nui gracefully"] = function() + -- Mock nui to not exist + package.loaded["nui.popup"] = nil + package.loaded["nui.input"] = nil + + local ui = require("code-review.ui") + + -- Try to show UI which uses nui if available + local ok = pcall(ui.show_comment_input, function() end) + + -- Should handle gracefully + MiniTest.expect.equality(type(ok), "boolean") +end + +return T diff --git a/tests/test_integration.lua b/tests/test_integration.lua new file mode 100644 index 0000000..d949a00 --- /dev/null +++ b/tests/test_integration.lua @@ -0,0 +1,125 @@ +-- Simplified integration tests that work reliably +local T = MiniTest.new_set() +local helpers = require("tests.helpers") + +-- Setup and teardown +T.hooks = { + pre_once = function() + -- Load plugin with memory backend to avoid file system + require("code-review").setup({ + comment = { + storage = { backend = "memory" }, + }, + }) + end, + + pre_case = function() + -- Reset and reinitialize for clean state + local state = require("code-review.state") + local memory = require("code-review.storage.memory") + + -- Use _reset for complete cleanup + state._reset() + memory._reset() + + -- Reinitialize + state.init() + + -- Clear any existing comments + state.clear() + end, +} + +-- Test basic formatter integration +T["formatter integration"] = function() + local formatter = require("code-review.formatter") + + -- Test data + local test_comments = { + { + file = "test1.lua", + line_start = 1, + line_end = 5, + comment = "First test comment", + timestamp = os.time(), + }, + { + file = "test1.lua", + line_start = 10, + line_end = 10, + comment = "Second test comment", + timestamp = os.time(), + }, + { + file = "test2.lua", + line_start = 20, + line_end = 25, + comment = "Third test comment", + timestamp = os.time(), + }, + } + + -- Format and parse + local formatted = formatter.format(test_comments) + local parsed = formatter.parse(formatted) + + -- Verify round-trip + MiniTest.expect.equality(#parsed, 3) + MiniTest.expect.equality(parsed[1].file, "test1.lua") + MiniTest.expect.equality(parsed[1].comment, "First test comment") + MiniTest.expect.equality(parsed[2].file, "test1.lua") + MiniTest.expect.equality(parsed[2].comment, "Second test comment") + MiniTest.expect.equality(parsed[3].file, "test2.lua") + MiniTest.expect.equality(parsed[3].comment, "Third test comment") +end + +-- Test comment formatting +T["comment formatting"] = function() + local comment = require("code-review.comment") + + local test_data = { + file = "test.lua", + line_start = 10, + line_end = 15, + comment = "This is a test comment\nWith multiple lines", + context_lines = { "function test()", " return true", "end" }, + } + + -- Format as markdown + local lines = comment.format_as_markdown(test_data, true, false) + + -- Verify format + MiniTest.expect.equality(type(lines), "table") + MiniTest.expect.equality(#lines > 0, true) + + -- Check content + local content = table.concat(lines, "\n") + helpers.expect.match(content, "test.lua:10%-15") + helpers.expect.match(content, "This is a test comment") + helpers.expect.match(content, "With multiple lines") + helpers.expect.match(content, "function test") +end + +-- Test utils functions +T["utils integration"] = function() + local utils = require("code-review.utils") + + -- Test path normalization + local paths = { + "/absolute/path/file.lua", + "relative/path/file.lua", + "~/home/file.lua", + } + + for _, path in ipairs(paths) do + local normalized = utils.normalize_path(path) + MiniTest.expect.equality(type(normalized), "string") + MiniTest.expect.equality(#normalized > 0, true) + end + + -- Test filename generation + local filename = utils.generate_filename("markdown") + helpers.expect.match(filename, "code%-review%-.*%.md") +end + +return T