From 067e4132808be4838bdc10bb1d549af81a21b9a4 Mon Sep 17 00:00:00 2001 From: Simon McLean Date: Sat, 14 Sep 2024 18:44:38 +0100 Subject: [PATCH] various fixes and tests --- lua/triptych/actions.lua | 72 ++++++++++++++------- lua/triptych/autocmds.lua | 6 ++ test_framework/queue.lua | 20 +++--- test_framework/test.lua | 19 +++--- tests/specs/ui_spec.lua | 133 +++++++++++++++++++++++++++++--------- tests/utils.lua | 34 ++++++++-- 6 files changed, 208 insertions(+), 76 deletions(-) diff --git a/lua/triptych/actions.lua b/lua/triptych/actions.lua index f4d32e5..0153139 100644 --- a/lua/triptych/actions.lua +++ b/lua/triptych/actions.lua @@ -4,6 +4,7 @@ local view = require 'triptych.view' local plenary_path = require 'plenary.path' local triptych_help = require 'triptych.help' local autocmds = require 'triptych.autocmds' +local log = require 'triptych.logger' local Actions = {} @@ -44,6 +45,49 @@ local function rename_node_and_publish(from, to) end end +---Wraps plenary Path:copy with public events +---Note: It only publishes events for files, not folders. This is probably fine for LSP purposes +---@param target PathDetails +---@param destination string +---@return nil +local function duplicate_node_and_publish(target, destination) + log.debug('duplicate_file_or_dir', { target = target, destination = destination }) + if not target.is_dir then + autocmds.publish_will_create_node(destination) + end + + local p = plenary_path:new(target.path) + -- Note: Plenary has a bug whereby a copying a directory into itself creates hundreds of nested copies + -- https://github.com/nvim-lua/plenary.nvim/pull/358 + local results = p:copy { + destination = destination, + recursive = true, + override = false, + interactive = true, + } + + local files_created = {} + + local function handle_results(results) + for key, value in pairs(results) do + if type(value) == 'table' then + handle_results(value) + elseif value then + table.insert(files_created, key.filename) + end + end + end + + handle_results(results) + + -- Sorting to avoid flakey test + table.sort(files_created) + + for _, path in ipairs(files_created) do + autocmds.publish_did_create_node(path) + end +end + --- TODO: Return type ---@param State TriptychState ---@param refresh_view fun(): nil @@ -188,25 +232,6 @@ function Actions.new(State, refresh_view) refresh_view() end - ---@param target PathDetails - ---@param destination string - ---@param callback? fun(boolean) - Callback indicting whether an item an copied - ---@return nil - M.duplicate_file_or_dir = function(target, destination, callback) - local p = plenary_path:new(target.path) - local results = p:copy { - destination = destination, - recursive = true, - override = false, - interactive = true, - } - if callback then - for _, v in pairs(results) do - callback(v) - end - end - end - M.bulk_toggle_copy = function() local targets = view.get_targets_in_selection(State) local contains_copy_items = false @@ -277,9 +302,9 @@ function Actions.new(State, refresh_view) return get_copy_path(target_path, i + 1) end + -- TODO: This function is a mess. Maybe break out into handlers for copy and cut? ---@return nil M.paste = function() - -- TODO: This function is a mess. Maybe break out into handlers for copy and cut? local cursor_target = view.get_target_under_cursor(State) local destination_dir = u.eval(function() if not cursor_target then @@ -305,8 +330,11 @@ function Actions.new(State, refresh_view) -- Handle copy items for _, item in ipairs(State.copy_list) do local destination = get_copy_path(u.path_join(destination_dir, item.display_name)) - M.duplicate_file_or_dir(item, destination) - autocmds.publish_did_delete_node(destination) + if item.is_dir then + -- Strip trailing slash + destination = string.sub(destination, 1, #destination - 1) + end + duplicate_node_and_publish(item, destination) end view.jump_cursor_to(State, destination_dir) diff --git a/lua/triptych/autocmds.lua b/lua/triptych/autocmds.lua index d9eac0f..5856306 100644 --- a/lua/triptych/autocmds.lua +++ b/lua/triptych/autocmds.lua @@ -147,6 +147,9 @@ end ---@param path string function M.publish_will_create_node(path) + -- vim.print { + -- will_create = path + -- } exec_public_autocmd('TriptychWillCreateNode', { path = path, }) @@ -154,6 +157,9 @@ end ---@param path string function M.publish_did_create_node(path) + -- vim.print { + -- created = path + -- } exec_public_autocmd('TriptychDidCreateNode', { path = path, }) diff --git a/test_framework/queue.lua b/test_framework/queue.lua index 2cfaee9..c06ca10 100644 --- a/test_framework/queue.lua +++ b/test_framework/queue.lua @@ -39,11 +39,11 @@ end function TestQueue:cleanup() self.is_running = false for _, test in ipairs(self.queue) do - test:cleanup() + test:reset() test.result = nil end for _, test in ipairs(self.completed) do - test:cleanup() + test:reset() test.result = nil end self.queue = {} @@ -104,13 +104,15 @@ end ---@param fail_message? string function TestQueue:handle_test_fail(test, fail_message) test.result = 'failed' - u.print('[FAILED] ' .. test.name) - error(fail_message) - self:cleanup() - if u.is_headless() then - u.exit_status_code 'failed' - else - end + vim.schedule(function() + u.print('[FAILED] ' .. test.name) + error(fail_message) + test:reset() + self:cleanup() + if u.is_headless() then + u.exit_status_code 'failed' + end + end) end function TestQueue:run_next() diff --git a/test_framework/test.lua b/test_framework/test.lua index 053a0a3..ad449f7 100644 --- a/test_framework/test.lua +++ b/test_framework/test.lua @@ -70,28 +70,31 @@ end ---Run an asyncronous test ---@param callback fun(passed: boolean, fail_message?: string) function Test:run_async(callback) - local success, err = pcall(self.test_body_async, function(test_callback) + local success, err = pcall(self.test_body_async, function(test_finish) if self.has_run then error 'Attempted to invoke test completion more than once. Check that any async callbacks in the test are not firing multiple times.' end + -- Scheduling this allows cleanup to complete before running the next test + local scheduled_callback = vim.schedule_wrap(callback) + self.has_run = true - if test_callback.cleanup then - local cleanup_successful, cleanup_err = pcall(test_callback.cleanup) + if test_finish.cleanup then + local cleanup_successful, cleanup_err = pcall(test_finish.cleanup) if not cleanup_successful then error(cleanup_err) end end if self.is_timed_out then - callback(false, 'Timed out') + scheduled_callback(false, 'Timed out') else - local passed, fail_message = pcall(test_callback.assertions) + local passed, fail_message = pcall(test_finish.assertions) if passed then - callback(true, nil) + scheduled_callback(true, nil) else - callback(false, fail_message) + scheduled_callback(false, fail_message) end end end) @@ -123,7 +126,7 @@ function Test:only() return self end -function Test:cleanup() +function Test:reset() self.has_run = false self.result = nil end diff --git a/tests/specs/ui_spec.lua b/tests/specs/ui_spec.lua index 52537ca..2e683b8 100644 --- a/tests/specs/ui_spec.lua +++ b/tests/specs/ui_spec.lua @@ -254,8 +254,7 @@ describe('Triptych UI', { end) end), - -- Having trouble with this - -- How to programatically input a selection in vim.ui.select + -- TODO: Having trouble with this. How to programatically input a selection in vim.ui.select? test('deletes files and folders', function(done) local expected_lines = { primary = { @@ -291,8 +290,86 @@ describe('Triptych UI', { end) end):skip(), - -- TODO: Skipped this because there seems to be a bug with dir pasting! - test('copies file and folders', function(done) + test('copies folders along with their contents', function(done) + local expected_lines = { + primary = { + 'level_3/', + 'level_4/', -- this is the copied dir, cursor should be over it + 'level_2_file_1.lua', + }, + child = { + 'level_5/', + 'level_4_file_1.lua', + }, + } + open_triptych(function() + -- Copy directory under cursor + u.press_keys 'c' + u.on_primary_window_updated(function() + -- Move left into the parent directory + u.press_keys 'h' + u.on_all_wins_updated(function() + -- Move down, so that the cursor is not over a directory, then paste the previously copied directory + u.press_keys 'jp' + u.on_all_wins_updated(function() + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines.child, state.lines.child) + assert.same(expected_lines.primary, state.lines.primary) + end, + cleanup = function() + vim.fn.delete(u.join_path(cwd, 'tests/test_playground/level_1/level_2/level_4'), 'rf') + end, + } + end) + end) + end) + end) + end) + end), + + test('when copying-pasting, publishes public events for files (not folders)', function(done) + local base_dir = u.join_path(cwd, 'tests/test_playground/level_1/level_2') + + local expected_events = { + ['TriptychDidCreateNode'] = { + { path = u.join_path(base_dir, 'level_4/level_4_file_1.lua') }, + { path = u.join_path(base_dir, 'level_4/level_5/level_5_file_1.lua') }, + }, + } + + open_triptych(function() + -- Copy directory under cursor + u.press_keys 'c' + u.on_primary_window_updated(function() + -- Move left into the parent directory + u.press_keys 'h' + u.on_all_wins_updated(function() + -- Move down, so that the cursor is not over a directory, then paste the previously copied directory + u.press_keys 'jp' + u.on_events({ + { name = 'TriptychDidCreateNode', wait_for_n = 2 }, + }, function(events) + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_events, events) + end, + cleanup = function() + vim.fn.delete(u.join_path(cwd, 'tests/test_playground/level_1/level_2/level_4'), 'rf') + end, + } + end) + end) + end) + end) + end) + end), + + test('copies files', function(done) local expected_lines = { primary = { 'level_4/', @@ -300,51 +377,47 @@ describe('Triptych UI', { 'level_3_file_1_copy1.md', }, child = { - 'level_4/', 'level_5/', + 'level_3_file_1.md', 'level_4_file_1.lua', }, } + open_triptych(function() - u.press_keys 'c' + -- Move down to the file and copy + u.press_keys 'jc' u.on_primary_window_updated(function() + -- Paste u.press_keys 'p' u.on_primary_window_updated(function() - u.press_keys 'j' - u.press_keys 'c' + -- Copy file again, move up over directory + u.press_keys 'ck' u.on_primary_window_updated(function() + -- Paste u.press_keys 'p' - u.on_primary_window_updated(function() - -- Go back to the top, so we can the new dir we've pasted in the the child directory - u.press_keys 'gg' - u.on_child_window_updated(function() - local state = u.get_state() - close_triptych(function() - done { - assertions = function() - assert.same(expected_lines.child, state.lines.child) - assert.same(expected_lines.primary, state.lines.primary) - end, - cleanup = function() - vim.fn.delete(u.join_path(opening_dir, 'level_3_file_1_copy1.md')) - vim.fn.delete(u.join_path(opening_dir, 'level_4/level_4', 'rf')) - end, - } - end) + u.on_child_window_updated(function() + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines.primary, state.lines.primary) + assert.same(expected_lines.child, state.lines.child) + end, + cleanup = function() + vim.fn.delete(u.join_path(opening_dir, 'level_3_file_1_copy1.md')) + vim.fn.delete(u.join_path(opening_dir, 'level_4/level_3_file_1.md')) + end, + } end) end) end) end) end) end) - end):skip(), + end), - -- TODO: This once the dir pasting bug has been fixed -- test('moves files and folders', function (done) end):skip() - -- TODO: This once the dir pasting bug has been fixed - -- test('copies files and folders', function(done) end):skip() - test('renames files and folders', function(done) local expected_lines = { primary = { diff --git a/tests/utils.lua b/tests/utils.lua index eb05f89..2012c10 100644 --- a/tests/utils.lua +++ b/tests/utils.lua @@ -23,7 +23,7 @@ function M.on_event(event, callback, once) if once == nil then once = true end - vim.api.nvim_create_autocmd('User', { + return vim.api.nvim_create_autocmd('User', { group = 'TriptychEvents', pattern = event, once = once, @@ -52,24 +52,44 @@ function M.on_events(events, callback) return true end + local function cleanup_autocmds() + vim.print { + autocmd_ids = autocmd_ids + } + for _, id in ipairs(autocmd_ids) do + api.nvim_del_autocmd(id) + end + end + for _, event in ipairs(events) do - result[event.name] = {} + local event_name = event.name + local expected_count = event.wait_for_n + + result[event_name] = {} local timer = vim.loop.new_timer() - local id = M.on_event(event.name, function(data) + local id = M.on_event(event_name, function(data) + vim.print { + event_name = event_name, + count = #result[event_name] + 1, + data = data, + } timer:stop() - table.insert(result[event.name], data.data) + table.insert(result[event_name], data.data) + + if #result[event_name] > expected_count then + cleanup_autocmds() + error('Expected ' .. expected_count .. ' events of type "' .. event_name .. '", but that number was exceeded') + end if is_ready() then timer:start( 1000, 0, vim.schedule_wrap(function() - for _, id in ipairs(autocmd_ids) do - api.nvim_del_autocmd(id) - end + cleanup_autocmds() callback(result) end) )