refactor: Refactor everything
BREAKING CHANGE: Actually move to the next pair of quotes
This commit is contained in:
parent
9622219679
commit
6d065862b0
6 changed files with 1044 additions and 27 deletions
|
@ -1,3 +1,5 @@
|
|||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
|
|
105
Makefile
Normal file
105
Makefile
Normal file
|
@ -0,0 +1,105 @@
|
|||
# JQuote Plugin Makefile
|
||||
# Author: Jan
|
||||
# License: MIT
|
||||
|
||||
.PHONY: test test-verbose lint format check clean install help
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# Colors for output
|
||||
BOLD := \033[1m
|
||||
RED := \033[31m
|
||||
GREEN := \033[32m
|
||||
YELLOW := \033[33m
|
||||
BLUE := \033[34m
|
||||
RESET := \033[0m
|
||||
|
||||
# Project directories
|
||||
LUA_DIR := lua
|
||||
TEST_DIR := tests
|
||||
PLUGIN_DIR := plugin
|
||||
|
||||
# Lua interpreter
|
||||
LUA := lua
|
||||
|
||||
## Run all tests
|
||||
test:
|
||||
@echo "$(BLUE)$(BOLD)Running JQuote Plugin Tests...$(RESET)"
|
||||
@cd $(PWD) && $(LUA) tests/test_runner.lua
|
||||
|
||||
## Run tests with verbose output
|
||||
test-verbose: test
|
||||
@echo "$(GREEN)Test execution completed$(RESET)"
|
||||
|
||||
## Check code quality (basic syntax check)
|
||||
check:
|
||||
@echo "$(BLUE)$(BOLD)Checking Lua syntax...$(RESET)"
|
||||
@for file in $$(find $(LUA_DIR) -name "*.lua"); do \
|
||||
echo "Checking $$file..."; \
|
||||
$(LUA) -e "loadfile('$$file')"; \
|
||||
if [ $$? -ne 0 ]; then \
|
||||
echo "$(RED)Syntax error in $$file$(RESET)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
@echo "$(GREEN)All Lua files have valid syntax$(RESET)"
|
||||
|
||||
## Format code (placeholder - would use stylua if available)
|
||||
format:
|
||||
@echo "$(YELLOW)Code formatting would require stylua or similar tool$(RESET)"
|
||||
@echo "$(YELLOW)Install with: cargo install stylua$(RESET)"
|
||||
|
||||
## Run linter (placeholder - would use luacheck if available)
|
||||
lint:
|
||||
@echo "$(YELLOW)Linting would require luacheck$(RESET)"
|
||||
@echo "$(YELLOW)Install with: luarocks install luacheck$(RESET)"
|
||||
|
||||
## Clean temporary files
|
||||
clean:
|
||||
@echo "$(BLUE)Cleaning temporary files...$(RESET)"
|
||||
@find . -name "*.tmp" -delete 2>/dev/null || true
|
||||
@find . -name "*.log" -delete 2>/dev/null || true
|
||||
@echo "$(GREEN)Cleanup completed$(RESET)"
|
||||
|
||||
## Install plugin (symlink to Neovim config)
|
||||
install:
|
||||
@echo "$(BLUE)$(BOLD)Installing JQuote Plugin...$(RESET)"
|
||||
@if [ -z "$$XDG_CONFIG_HOME" ]; then \
|
||||
NVIM_CONFIG="$$HOME/.config/nvim"; \
|
||||
else \
|
||||
NVIM_CONFIG="$$XDG_CONFIG_HOME/nvim"; \
|
||||
fi; \
|
||||
if [ ! -d "$$NVIM_CONFIG" ]; then \
|
||||
echo "$(RED)Neovim config directory not found: $$NVIM_CONFIG$(RESET)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
PLUGIN_TARGET="$$NVIM_CONFIG/lua/jquote"; \
|
||||
if [ -L "$$PLUGIN_TARGET" ] || [ -d "$$PLUGIN_TARGET" ]; then \
|
||||
echo "$(YELLOW)Plugin already installed, removing old version...$(RESET)"; \
|
||||
rm -rf "$$PLUGIN_TARGET"; \
|
||||
fi; \
|
||||
ln -s "$(PWD)/$(LUA_DIR)/jquote" "$$PLUGIN_TARGET"; \
|
||||
echo "$(GREEN)Plugin installed successfully to $$PLUGIN_TARGET$(RESET)"
|
||||
|
||||
## Run development checks (syntax + tests)
|
||||
dev-check: check test
|
||||
@echo "$(GREEN)$(BOLD)All development checks passed!$(RESET)"
|
||||
|
||||
## Show help
|
||||
help:
|
||||
@echo "$(BOLD)JQuote Plugin Development Commands$(RESET)"
|
||||
@echo ""
|
||||
@echo "$(BOLD)Available targets:$(RESET)"
|
||||
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ { printf " $(BLUE)%-15s$(RESET) %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
|
||||
@echo ""
|
||||
@echo "$(BOLD)Usage Examples:$(RESET)"
|
||||
@echo " make test # Run all tests"
|
||||
@echo " make check # Check syntax"
|
||||
@echo " make dev-check # Run all development checks"
|
||||
@echo " make install # Install plugin to Neovim"
|
||||
@echo ""
|
||||
|
||||
## Quick development workflow
|
||||
quick: clean check test
|
||||
@echo "$(GREEN)$(BOLD)Quick development check completed!$(RESET)"
|
|
@ -5,9 +5,24 @@
|
|||
|
||||
local M = {}
|
||||
|
||||
--- Plugin constants
|
||||
--- @type table
|
||||
local CONSTANTS = {
|
||||
JUMP_BEHAVIORS = {
|
||||
AUTO = "auto",
|
||||
MANUAL = "manual",
|
||||
},
|
||||
LOG_LEVELS = vim.log.levels,
|
||||
PLUGIN_NAME = "JQuote",
|
||||
MIN_QUOTE_CHARS = 1,
|
||||
MAX_LINE_LENGTH = 10000, -- Safety limit for line processing
|
||||
VERSION_FALLBACK = "unknown",
|
||||
}
|
||||
|
||||
--- @class JQuoteOptions
|
||||
--- @field hotkey string The hotkey for toggling quotes (default: "<leader>tq")
|
||||
--- @field quote_chars string[] List of quote characters to cycle through
|
||||
--- @field quote_chars string[] List of quote characters to cycle through (minimum 1 character required)
|
||||
--- @field jump_behavior "auto"|"manual" Whether to auto-jump to next pair when not inside quotes (default: "auto")
|
||||
local JQuoteOptions = {}
|
||||
|
||||
--- Default configuration options
|
||||
|
@ -15,6 +30,7 @@ local JQuoteOptions = {}
|
|||
local DEFAULT_OPTIONS = {
|
||||
hotkey = "<leader>tq",
|
||||
quote_chars = { "'", '"', "`" },
|
||||
jump_behavior = CONSTANTS.JUMP_BEHAVIORS.AUTO,
|
||||
}
|
||||
|
||||
--- Current plugin options
|
||||
|
@ -30,10 +46,17 @@ M._name = "jquote"
|
|||
--- @return boolean true if the character is a configured quote character, false otherwise
|
||||
--- @private
|
||||
local function is_defined_quote_char(char)
|
||||
-- Input validation with early returns
|
||||
if type(char) ~= "string" or #char ~= 1 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Defensive programming: ensure quote_chars exists and is valid
|
||||
if not M.options.quote_chars or type(M.options.quote_chars) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Use explicit loop for better readability and performance
|
||||
for _, quote_char in ipairs(M.options.quote_chars) do
|
||||
if char == quote_char then
|
||||
return true
|
||||
|
@ -42,6 +65,56 @@ local function is_defined_quote_char(char)
|
|||
return false
|
||||
end
|
||||
|
||||
--- Finds the next quote pair forwards from the current cursor position
|
||||
--- @param line string The text line to analyze
|
||||
--- @param cursor_col number The 0-based cursor column position
|
||||
--- @return number|nil start_index The 0-based start position of the opening quote
|
||||
--- @return number|nil end_index The 0-based end position of the closing quote
|
||||
--- @return string|nil quote_char The quote character used, or nil if not found
|
||||
--- @private
|
||||
local function find_next_quote_pair_forward(line, cursor_col)
|
||||
-- Comprehensive input validation
|
||||
if type(line) ~= "string" or type(cursor_col) ~= "number" or cursor_col < 0 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
-- Safety check for excessively long lines
|
||||
if #line > CONSTANTS.MAX_LINE_LENGTH then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- @type table[] Array of quote position records
|
||||
local quote_positions = {}
|
||||
|
||||
-- Scan line for quote characters starting from cursor position
|
||||
-- Use math.min to prevent out-of-bounds access
|
||||
local start_pos = math.min(cursor_col + 2, #line)
|
||||
for i = start_pos, #line do
|
||||
local char = line:sub(i, i)
|
||||
if is_defined_quote_char(char) then
|
||||
table.insert(quote_positions, {
|
||||
char = char,
|
||||
index = i - 1 -- Convert to 0-based indexing
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Find the first matching quote pair using optimized algorithm
|
||||
for i = 1, #quote_positions do
|
||||
local start_quote = quote_positions[i]
|
||||
for j = i + 1, #quote_positions do
|
||||
local end_quote = quote_positions[j]
|
||||
|
||||
-- Match opening and closing quotes of same type
|
||||
if start_quote.char == end_quote.char then
|
||||
return start_quote.index, end_quote.index, start_quote.char
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- Locates the quoted string boundaries containing the cursor position
|
||||
--- @param line string The text line to analyze
|
||||
--- @param cursor_col number The 0-based cursor column position
|
||||
|
@ -50,10 +123,16 @@ end
|
|||
--- @return string|nil quote_char The quote character used, or nil if not found
|
||||
--- @private
|
||||
local function find_quoted_string_at_cursor(line, cursor_col)
|
||||
-- Comprehensive input validation
|
||||
if type(line) ~= "string" or type(cursor_col) ~= "number" or cursor_col < 0 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
-- Safety check for excessively long lines
|
||||
if #line > CONSTANTS.MAX_LINE_LENGTH then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- @type table[] Array of quote position records
|
||||
local quote_positions = {}
|
||||
|
||||
|
@ -76,7 +155,8 @@ local function find_quoted_string_at_cursor(line, cursor_col)
|
|||
|
||||
-- Match opening and closing quotes of same type
|
||||
if start_quote.char == end_quote.char then
|
||||
if (cursor_col >= start_quote.index and cursor_col <= end_quote.index) or (cursor_col <= start_quote.index and cursor_col <= end_quote.index) then
|
||||
-- Improved cursor boundary check logic
|
||||
if cursor_col >= start_quote.index and cursor_col <= end_quote.index then
|
||||
return start_quote.index, end_quote.index, start_quote.char
|
||||
end
|
||||
|
||||
|
@ -91,17 +171,38 @@ local function find_quoted_string_at_cursor(line, cursor_col)
|
|||
return nil, nil, nil
|
||||
end
|
||||
|
||||
--- Utility function for logging with consistent formatting
|
||||
--- @param message string The message to log
|
||||
--- @param level number The log level (from vim.log.levels)
|
||||
--- @private
|
||||
local function log_message(message, level)
|
||||
if type(message) ~= "string" then
|
||||
message = tostring(message)
|
||||
end
|
||||
local formatted_message = string.format("%s: %s", CONSTANTS.PLUGIN_NAME, message)
|
||||
vim.notify(formatted_message, level or CONSTANTS.LOG_LEVELS.INFO)
|
||||
end
|
||||
|
||||
--- Determines the next quote character in the rotation sequence
|
||||
--- @param current_char string The current quote character
|
||||
--- @return string The next quote character in the configured sequence
|
||||
--- @private
|
||||
local function get_next_quote_char(current_char)
|
||||
-- Input validation with defensive programming
|
||||
if type(current_char) ~= "string" then
|
||||
return M.options.quote_chars[1]
|
||||
log_message("Invalid current_char type, using fallback", CONSTANTS.LOG_LEVELS.WARN)
|
||||
return M.options.quote_chars[1] or "'"
|
||||
end
|
||||
|
||||
-- Ensure quote_chars is valid
|
||||
if not M.options.quote_chars or type(M.options.quote_chars) ~= "table" or #M.options.quote_chars == 0 then
|
||||
log_message("Invalid quote_chars configuration, using fallback", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return "'"
|
||||
end
|
||||
|
||||
local quote_count = #M.options.quote_chars
|
||||
|
||||
-- Optimized search with explicit bounds checking
|
||||
for i = 1, quote_count do
|
||||
if M.options.quote_chars[i] == current_char then
|
||||
-- Cycle to next character (wrapping around to first if at end)
|
||||
|
@ -111,41 +212,65 @@ local function get_next_quote_char(current_char)
|
|||
end
|
||||
|
||||
-- Fallback to first quote character if current not found
|
||||
log_message("Current quote character not found in configuration, using first available", CONSTANTS.LOG_LEVELS.WARN)
|
||||
return M.options.quote_chars[1]
|
||||
end
|
||||
|
||||
--- Toggles quote characters around the string at the cursor position
|
||||
--- Changes quotes in a cyclic manner based on the configured quote_chars sequence
|
||||
--- @return boolean true if quotes were successfully toggled, false otherwise
|
||||
--- If cursor is not inside a quote pair, automatically jumps to the next pair forwards
|
||||
--- @return boolean true if quotes were successfully toggled or cursor moved, false otherwise
|
||||
function M.toggle_quotes()
|
||||
-- Enhanced error handling with detailed logging
|
||||
local success, current_line = pcall(vim.api.nvim_get_current_line)
|
||||
if not success or type(current_line) ~= "string" then
|
||||
vim.notify("JQuote: Failed to get current line", vim.log.levels.ERROR)
|
||||
log_message("Failed to get current line", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local success_cursor, cursor_pos = pcall(vim.api.nvim_win_get_cursor, 0)
|
||||
if not success_cursor or type(cursor_pos) ~= "table" or #cursor_pos < 2 then
|
||||
vim.notify("JQuote: Failed to get cursor position", vim.log.levels.ERROR)
|
||||
log_message("Failed to get cursor position", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local cursor_row, cursor_col = cursor_pos[1], cursor_pos[2]
|
||||
local start_idx, end_idx, current_char = find_quoted_string_at_cursor(current_line, cursor_col)
|
||||
|
||||
-- If cursor is not inside a quote pair, handle based on jump behavior setting
|
||||
if not start_idx or not end_idx or not current_char then
|
||||
vim.notify("JQuote: No quoted string found at cursor position", vim.log.levels.INFO)
|
||||
return false
|
||||
if M.options.jump_behavior == CONSTANTS.JUMP_BEHAVIORS.AUTO then
|
||||
local next_start_idx, next_end_idx, _ = find_next_quote_pair_forward(current_line, cursor_col)
|
||||
|
||||
if next_start_idx and next_end_idx then
|
||||
-- Move cursor to the start of the next quote pair
|
||||
local success_move = pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, next_start_idx })
|
||||
if success_move then
|
||||
log_message("Jumped to next quote pair", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return true
|
||||
else
|
||||
log_message("Failed to move cursor to next quote pair", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log_message("No quote pairs found forwards on current line", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log_message("No quoted string found at cursor position", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Toggle quotes for current pair
|
||||
local next_char = get_next_quote_char(current_char)
|
||||
if next_char == current_char then
|
||||
vim.notify("JQuote: No alternative quote character available", vim.log.levels.WARN)
|
||||
log_message("No alternative quote character available", CONSTANTS.LOG_LEVELS.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Construct new line with replaced quote characters
|
||||
local new_line = table.concat({
|
||||
-- Construct new line with replaced quote characters using safe string operations
|
||||
local success_construct, new_line = pcall(table.concat, {
|
||||
current_line:sub(1, start_idx),
|
||||
next_char,
|
||||
current_line:sub(start_idx + 2, end_idx),
|
||||
|
@ -153,14 +278,24 @@ function M.toggle_quotes()
|
|||
current_line:sub(end_idx + 2)
|
||||
})
|
||||
|
||||
local success_set = pcall(vim.api.nvim_set_current_line, new_line)
|
||||
if not success_set then
|
||||
vim.notify("JQuote: Failed to update line", vim.log.levels.ERROR)
|
||||
if not success_construct or type(new_line) ~= "string" then
|
||||
log_message("Failed to construct new line with updated quotes", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Restore cursor position
|
||||
pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, cursor_col })
|
||||
local success_set = pcall(vim.api.nvim_set_current_line, new_line)
|
||||
if not success_set then
|
||||
log_message("Failed to update line", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Restore cursor position with error handling
|
||||
local success_restore = pcall(vim.api.nvim_win_set_cursor, 0, { cursor_row, cursor_col })
|
||||
if not success_restore then
|
||||
log_message("Warning: Failed to restore cursor position", CONSTANTS.LOG_LEVELS.WARN)
|
||||
end
|
||||
|
||||
log_message("Quote characters toggled successfully", CONSTANTS.LOG_LEVELS.INFO)
|
||||
return true
|
||||
end
|
||||
|
||||
|
@ -183,8 +318,8 @@ local function validate_options(options)
|
|||
return false, "quote_chars must be a table"
|
||||
end
|
||||
|
||||
if #options.quote_chars == 0 then
|
||||
return false, "quote_chars cannot be empty"
|
||||
if #options.quote_chars < CONSTANTS.MIN_QUOTE_CHARS then
|
||||
return false, string.format("quote_chars must contain at least %d character(s)", CONSTANTS.MIN_QUOTE_CHARS)
|
||||
end
|
||||
|
||||
for i, char in ipairs(options.quote_chars) do
|
||||
|
@ -194,6 +329,16 @@ local function validate_options(options)
|
|||
end
|
||||
end
|
||||
|
||||
if options.jump_behavior then
|
||||
if type(options.jump_behavior) ~= "string" then
|
||||
return false, "jump_behavior must be a string"
|
||||
end
|
||||
|
||||
if options.jump_behavior ~= CONSTANTS.JUMP_BEHAVIORS.AUTO and options.jump_behavior ~= CONSTANTS.JUMP_BEHAVIORS.MANUAL then
|
||||
return false, "jump_behavior must be either 'auto' or 'manual'"
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
|
@ -203,25 +348,25 @@ end
|
|||
function M.setup(user_options)
|
||||
user_options = user_options or {}
|
||||
|
||||
-- Validate user options
|
||||
-- Validate user options with enhanced error reporting
|
||||
local is_valid, error_msg = validate_options(user_options)
|
||||
if not is_valid then
|
||||
vim.notify(string.format("JQuote setup error: %s", error_msg), vim.log.levels.ERROR)
|
||||
log_message(string.format("Setup error: %s", error_msg), CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Merge with defaults using safe deep extend
|
||||
-- Merge with defaults using safe deep extend with enhanced error handling
|
||||
local success, merged_options = pcall(vim.tbl_deep_extend, "force", vim.deepcopy(DEFAULT_OPTIONS), user_options)
|
||||
if not success then
|
||||
vim.notify("JQuote: Failed to merge configuration options", vim.log.levels.ERROR)
|
||||
log_message("Failed to merge configuration options", CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
M.options = merged_options
|
||||
|
||||
-- Defensive check for quote_chars integrity
|
||||
if not M.options.quote_chars or #M.options.quote_chars == 0 then
|
||||
vim.notify("JQuote: quote_chars corrupted, restoring defaults", vim.log.levels.WARN)
|
||||
-- Enhanced defensive check for quote_chars integrity
|
||||
if not M.options.quote_chars or type(M.options.quote_chars) ~= "table" or #M.options.quote_chars < CONSTANTS.MIN_QUOTE_CHARS then
|
||||
log_message("quote_chars corrupted or invalid, restoring defaults", CONSTANTS.LOG_LEVELS.WARN)
|
||||
M.options.quote_chars = vim.deepcopy(DEFAULT_OPTIONS.quote_chars)
|
||||
end
|
||||
|
||||
|
@ -234,14 +379,45 @@ function M.setup(user_options)
|
|||
|
||||
local success_keymap = pcall(vim.keymap.set, "n", M.options.hotkey, M.toggle_quotes, keymap_opts)
|
||||
if not success_keymap then
|
||||
vim.notify(string.format("JQuote: Failed to set keymap for '%s'", M.options.hotkey), vim.log.levels.ERROR)
|
||||
log_message(string.format("Failed to set keymap for '%s'", M.options.hotkey), CONSTANTS.LOG_LEVELS.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
vim.notify(string.format("JQuote: Initialized with hotkey '%s'", M.options.hotkey), vim.log.levels.INFO)
|
||||
log_message(string.format("Initialized with hotkey '%s'", M.options.hotkey), CONSTANTS.LOG_LEVELS.INFO)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Gets the plugin version from git tags at runtime
|
||||
--- @return string version The current plugin version or "unknown" if unavailable
|
||||
function M.get_version()
|
||||
local success, handle = pcall(io.popen, "git describe --tags --abbrev=0 2>/dev/null")
|
||||
if not success or not handle then
|
||||
return CONSTANTS.VERSION_FALLBACK
|
||||
end
|
||||
|
||||
local version = handle:read("*a")
|
||||
local close_success = pcall(handle.close, handle)
|
||||
|
||||
if not close_success then
|
||||
log_message("Warning: Failed to close git command handle", CONSTANTS.LOG_LEVELS.WARN)
|
||||
end
|
||||
|
||||
if version and type(version) == "string" and #version > 0 then
|
||||
-- Remove trailing whitespace and newlines
|
||||
version = version:gsub("%s+$", "")
|
||||
-- Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
|
||||
if version:match("^v%d") then
|
||||
version = version:sub(2)
|
||||
end
|
||||
-- Additional validation for semantic versioning format
|
||||
if version:match("^%d+%.%d+%.%d+") then
|
||||
return version
|
||||
end
|
||||
end
|
||||
|
||||
return CONSTANTS.VERSION_FALLBACK
|
||||
end
|
||||
|
||||
--- Health check function for plugin diagnostics
|
||||
--- @return table health_info Plugin health information
|
||||
function M.health()
|
||||
|
@ -249,6 +425,8 @@ function M.health()
|
|||
options = M.options,
|
||||
quote_chars_count = M.options.quote_chars and #M.options.quote_chars or 0,
|
||||
hotkey_configured = M.options.hotkey ~= nil,
|
||||
jump_behavior_valid = M.options.jump_behavior == CONSTANTS.JUMP_BEHAVIORS.AUTO or M.options.jump_behavior == CONSTANTS.JUMP_BEHAVIORS.MANUAL,
|
||||
plugin_version = M.get_version(),
|
||||
}
|
||||
|
||||
return health_info
|
||||
|
|
122
tests/README.md
Normal file
122
tests/README.md
Normal file
|
@ -0,0 +1,122 @@
|
|||
# JQuote Plugin Tests
|
||||
|
||||
This directory contains the comprehensive test suite for the JQuote Neovim plugin.
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `init_spec.lua` - Main test file containing all unit tests
|
||||
- `test_runner.lua` - Custom lightweight test runner
|
||||
- `README.md` - This file
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Using Make (Recommended)
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run tests with verbose output
|
||||
make test-verbose
|
||||
|
||||
# Run development checks (syntax + tests)
|
||||
make dev-check
|
||||
```
|
||||
|
||||
### Direct Execution
|
||||
```bash
|
||||
# Run tests directly
|
||||
lua tests/test_runner.lua
|
||||
|
||||
# Or from project root
|
||||
cd /path/to/jquote.nvim && lua tests/test_runner.lua
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### 1. Plugin Initialization Tests
|
||||
- Default configuration validation
|
||||
- User option merging
|
||||
- Configuration validation
|
||||
- Key mapping setup
|
||||
|
||||
### 2. Quote Detection Tests
|
||||
- Finding quoted strings at cursor
|
||||
- Quote character cycling
|
||||
- Nested quote handling
|
||||
- Multiple quote pairs
|
||||
|
||||
### 3. Auto Jump Functionality Tests
|
||||
- Auto jump to next quote pair
|
||||
- Manual mode behavior
|
||||
- Edge case handling
|
||||
|
||||
### 4. Error Handling Tests
|
||||
- API failure scenarios
|
||||
- Invalid input handling
|
||||
- Long line safety limits
|
||||
- Resource cleanup
|
||||
|
||||
### 5. Version Management Tests
|
||||
- Git tag version extraction
|
||||
- Fallback version handling
|
||||
- Semantic version validation
|
||||
|
||||
### 6. Health Check Tests
|
||||
- Configuration validation
|
||||
- Plugin state reporting
|
||||
- Diagnostic information
|
||||
|
||||
### 7. Edge Cases Tests
|
||||
- Boundary conditions
|
||||
- Empty configurations
|
||||
- Single character strings
|
||||
- Line boundary quotes
|
||||
|
||||
## Test Output
|
||||
|
||||
The test runner provides colored output:
|
||||
- 🟢 Green: Passed tests
|
||||
- 🔴 Red: Failed tests
|
||||
- 🔵 Blue: Test categories
|
||||
- 🟡 Yellow: Warnings and info
|
||||
|
||||
## Mock System
|
||||
|
||||
Tests use a comprehensive vim API mock that simulates:
|
||||
- Line content and cursor position
|
||||
- Notification system
|
||||
- Key mapping
|
||||
- Configuration deep merging
|
||||
- API error scenarios
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add new tests, follow this pattern:
|
||||
|
||||
```lua
|
||||
describe("New Feature", function()
|
||||
it("should do something specific", function()
|
||||
-- Setup
|
||||
mock_vim._set_line_and_cursor("test line", 5)
|
||||
|
||||
-- Execute
|
||||
local result = jquote.some_function()
|
||||
|
||||
-- Assert
|
||||
assert.is_true(result)
|
||||
assert.are.equal("expected", mock_vim._current_line)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Isolation**: Each test should be independent
|
||||
2. **Clear Names**: Use descriptive test names
|
||||
3. **Setup/Teardown**: Use `before_each` for test setup
|
||||
4. **Comprehensive**: Test both success and failure paths
|
||||
5. **Edge Cases**: Include boundary condition tests
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
These tests are designed to run in CI environments and provide detailed feedback for development workflows.
|
393
tests/init_spec.lua
Normal file
393
tests/init_spec.lua
Normal file
|
@ -0,0 +1,393 @@
|
|||
--- JQuote Unit Tests
|
||||
--- @author Jan
|
||||
--- @license MIT
|
||||
|
||||
-- Create global state for mock
|
||||
local test_state = {
|
||||
current_line = "",
|
||||
cursor_row = 1,
|
||||
cursor_col = 0,
|
||||
notifications = {},
|
||||
keymaps = {}
|
||||
}
|
||||
|
||||
-- Mock vim API for testing
|
||||
local mock_vim = {
|
||||
log = {
|
||||
levels = {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
}
|
||||
},
|
||||
notify = function(message, level)
|
||||
table.insert(test_state.notifications, { message = message, level = level })
|
||||
end,
|
||||
api = {
|
||||
nvim_get_current_line = function()
|
||||
return test_state.current_line
|
||||
end,
|
||||
nvim_win_get_cursor = function()
|
||||
return { test_state.cursor_row, test_state.cursor_col }
|
||||
end,
|
||||
nvim_set_current_line = function(line)
|
||||
test_state.current_line = line
|
||||
return true
|
||||
end,
|
||||
nvim_win_set_cursor = function(win, pos)
|
||||
test_state.cursor_row = pos[1]
|
||||
test_state.cursor_col = pos[2]
|
||||
return true
|
||||
end,
|
||||
},
|
||||
keymap = {
|
||||
set = function(mode, key, func, opts)
|
||||
test_state.keymaps[key] = { mode = mode, func = func, opts = opts }
|
||||
return true
|
||||
end,
|
||||
},
|
||||
tbl_deep_extend = function(behavior, ...)
|
||||
local result = {}
|
||||
for _, tbl in ipairs({...}) do
|
||||
for k, v in pairs(tbl) do
|
||||
if type(v) == "table" and type(result[k]) == "table" then
|
||||
result[k] = mock_vim.tbl_deep_extend(behavior, result[k], v)
|
||||
else
|
||||
result[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end,
|
||||
deepcopy = nil, -- Will be set after mock_vim is defined
|
||||
-- Test utilities
|
||||
_reset = function()
|
||||
test_state.current_line = ""
|
||||
test_state.cursor_row = 1
|
||||
test_state.cursor_col = 0
|
||||
test_state.notifications = {}
|
||||
test_state.keymaps = {}
|
||||
end,
|
||||
_get_notifications = function()
|
||||
return test_state.notifications
|
||||
end,
|
||||
_set_line_and_cursor = function(line, col)
|
||||
test_state.current_line = line
|
||||
test_state.cursor_col = col or 0
|
||||
end,
|
||||
_keymaps = test_state.keymaps
|
||||
}
|
||||
|
||||
-- Set up deepcopy function now that mock_vim is defined
|
||||
mock_vim.deepcopy = function(tbl)
|
||||
if type(tbl) ~= "table" then return tbl end
|
||||
local copy = {}
|
||||
for k, v in pairs(tbl) do
|
||||
if type(v) == "table" then
|
||||
copy[k] = mock_vim.deepcopy(v)
|
||||
else
|
||||
copy[k] = v
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Add current directory to Lua path for module loading
|
||||
package.path = package.path .. ";./?.lua;./?/init.lua"
|
||||
|
||||
-- Inject mock vim into global scope BEFORE loading the module
|
||||
_G.vim = mock_vim
|
||||
|
||||
-- Load the module under test
|
||||
local jquote = require('lua.jquote.init')
|
||||
|
||||
-- Test suite
|
||||
describe("JQuote Plugin", function()
|
||||
|
||||
before_each(function()
|
||||
mock_vim._reset()
|
||||
-- Reset plugin to default state
|
||||
jquote.setup({})
|
||||
end)
|
||||
|
||||
describe("Plugin Initialization", function()
|
||||
|
||||
it("should initialize with default options", function()
|
||||
local success = jquote.setup()
|
||||
assert.is_true(success)
|
||||
assert.are.equal("<leader>tq", jquote.options.hotkey)
|
||||
assert.are.same({ "'", '"', "`" }, jquote.options.quote_chars)
|
||||
assert.are.equal("auto", jquote.options.jump_behavior)
|
||||
end)
|
||||
|
||||
it("should merge user options with defaults", function()
|
||||
local user_options = {
|
||||
hotkey = "<leader>q",
|
||||
quote_chars = { "'", '"' },
|
||||
jump_behavior = "manual"
|
||||
}
|
||||
local success = jquote.setup(user_options)
|
||||
assert.is_true(success)
|
||||
assert.are.equal("<leader>q", jquote.options.hotkey)
|
||||
assert.are.same({ "'", '"' }, jquote.options.quote_chars)
|
||||
assert.are.equal("manual", jquote.options.jump_behavior)
|
||||
end)
|
||||
|
||||
it("should validate quote_chars configuration", function()
|
||||
local success = jquote.setup({ quote_chars = {} })
|
||||
assert.is_false(success)
|
||||
|
||||
local notifications = mock_vim._get_notifications()
|
||||
assert.is_true(#notifications > 0)
|
||||
assert.matches("quote_chars must contain at least", notifications[1].message)
|
||||
end)
|
||||
|
||||
it("should validate jump_behavior configuration", function()
|
||||
local success = jquote.setup({ jump_behavior = "invalid" })
|
||||
assert.is_false(success)
|
||||
|
||||
local notifications = mock_vim._get_notifications()
|
||||
assert.is_true(#notifications > 0)
|
||||
assert.matches("jump_behavior must be either", notifications[1].message)
|
||||
end)
|
||||
|
||||
it("should set up key mapping", function()
|
||||
jquote.setup({ hotkey = "<leader>test" })
|
||||
assert.is_not_nil(mock_vim._keymaps["<leader>test"])
|
||||
assert.are.equal("n", mock_vim._keymaps["<leader>test"].mode)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Quote Detection", function()
|
||||
|
||||
it("should find quoted string at cursor position", function()
|
||||
mock_vim._set_line_and_cursor("hello 'world' test", 8)
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_true(success)
|
||||
assert.are.equal('hello "world" test', test_state.current_line)
|
||||
end)
|
||||
|
||||
it("should cycle through quote characters", function()
|
||||
jquote.setup({ quote_chars = { "'", '"', "`" } })
|
||||
|
||||
-- First toggle: ' -> "
|
||||
mock_vim._set_line_and_cursor("hello 'world'", 8)
|
||||
jquote.toggle_quotes()
|
||||
assert.are.equal('hello "world"', test_state.current_line)
|
||||
|
||||
-- Second toggle: " -> `
|
||||
mock_vim._set_line_and_cursor('hello "world"', 8)
|
||||
jquote.toggle_quotes()
|
||||
assert.are.equal('hello `world`', test_state.current_line)
|
||||
|
||||
-- Third toggle: ` -> '
|
||||
mock_vim._set_line_and_cursor('hello `world`', 8)
|
||||
jquote.toggle_quotes()
|
||||
assert.are.equal("hello 'world'", test_state.current_line)
|
||||
end)
|
||||
|
||||
it("should handle nested quotes correctly", function()
|
||||
mock_vim._set_line_and_cursor('outer "inner \'nested\' text" end', 15)
|
||||
jquote.toggle_quotes()
|
||||
assert.are.equal('outer "inner "nested" text" end', test_state.current_line)
|
||||
end)
|
||||
|
||||
it("should handle multiple quote pairs on same line", function()
|
||||
mock_vim._set_line_and_cursor("'first' and 'second' pair", 2)
|
||||
jquote.toggle_quotes()
|
||||
assert.are.equal('"first" and \'second\' pair', test_state.current_line)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Auto Jump Functionality", function()
|
||||
|
||||
it("should jump to next quote pair when cursor is not inside quotes", function()
|
||||
jquote.setup({ jump_behavior = "auto" })
|
||||
mock_vim._set_line_and_cursor("hello 'world' and 'test'", 0)
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_true(success)
|
||||
assert.are.equal(6, test_state.cursor_col) -- Should jump to first quote
|
||||
end)
|
||||
|
||||
it("should not jump when jump_behavior is manual", function()
|
||||
jquote.setup({ jump_behavior = "manual" })
|
||||
mock_vim._set_line_and_cursor("hello 'world' test", 0)
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
assert.are.equal(0, test_state.cursor_col) -- Cursor should not move
|
||||
end)
|
||||
|
||||
it("should handle case with no quote pairs found", function()
|
||||
jquote.setup({ jump_behavior = "auto" })
|
||||
mock_vim._set_line_and_cursor("no quotes here", 0)
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
|
||||
local notifications = mock_vim._get_notifications()
|
||||
assert.is_true(#notifications > 0)
|
||||
assert.matches("No quote pairs found", notifications[1].message)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Error Handling", function()
|
||||
|
||||
it("should handle invalid line content gracefully", function()
|
||||
-- Mock API failure
|
||||
mock_vim.api.nvim_get_current_line = function()
|
||||
error("API Error")
|
||||
end
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
|
||||
local notifications = mock_vim._get_notifications()
|
||||
assert.is_true(#notifications > 0)
|
||||
assert.matches("Failed to get current line", notifications[1].message)
|
||||
end)
|
||||
|
||||
it("should handle cursor position API failure", function()
|
||||
mock_vim.api.nvim_win_get_cursor = function()
|
||||
error("Cursor API Error")
|
||||
end
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
|
||||
local notifications = mock_vim._get_notifications()
|
||||
assert.is_true(#notifications > 0)
|
||||
assert.matches("Failed to get cursor position", notifications[1].message)
|
||||
end)
|
||||
|
||||
it("should handle line update failure", function()
|
||||
mock_vim._set_line_and_cursor("'test'", 2)
|
||||
mock_vim.api.nvim_set_current_line = function()
|
||||
error("Set line error")
|
||||
end
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
|
||||
local notifications = mock_vim._get_notifications()
|
||||
assert.is_true(#notifications > 0)
|
||||
assert.matches("Failed to update line", notifications[1].message)
|
||||
end)
|
||||
|
||||
it("should handle excessively long lines", function()
|
||||
local long_line = string.rep("a", 15000) .. "'test'" -- Over MAX_LINE_LENGTH
|
||||
mock_vim._set_line_and_cursor(long_line, 10000)
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Version Management", function()
|
||||
|
||||
it("should return version from git tag", function()
|
||||
-- Mock io.popen to return a version
|
||||
local original_io_popen = io.popen
|
||||
io.popen = function(cmd)
|
||||
return {
|
||||
read = function() return "v1.2.3\n" end,
|
||||
close = function() return true end
|
||||
}
|
||||
end
|
||||
|
||||
local version = jquote.get_version()
|
||||
assert.are.equal("1.2.3", version)
|
||||
|
||||
-- Restore original function
|
||||
io.popen = original_io_popen
|
||||
end)
|
||||
|
||||
it("should return fallback version when git fails", function()
|
||||
local original_io_popen = io.popen
|
||||
io.popen = function(cmd)
|
||||
error("Git not available")
|
||||
end
|
||||
|
||||
local version = jquote.get_version()
|
||||
assert.are.equal("unknown", version)
|
||||
|
||||
io.popen = original_io_popen
|
||||
end)
|
||||
|
||||
it("should validate semantic version format", function()
|
||||
local original_io_popen = io.popen
|
||||
io.popen = function(cmd)
|
||||
return {
|
||||
read = function() return "invalid-version\n" end,
|
||||
close = function() return true end
|
||||
}
|
||||
end
|
||||
|
||||
local version = jquote.get_version()
|
||||
assert.are.equal("unknown", version)
|
||||
|
||||
io.popen = original_io_popen
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Health Check", function()
|
||||
|
||||
it("should return comprehensive health information", function()
|
||||
jquote.setup({
|
||||
hotkey = "<leader>test",
|
||||
quote_chars = { "'", '"' },
|
||||
jump_behavior = "auto"
|
||||
})
|
||||
|
||||
local health = jquote.health()
|
||||
|
||||
assert.is_table(health.options)
|
||||
assert.are.equal(2, health.quote_chars_count)
|
||||
assert.is_true(health.hotkey_configured)
|
||||
assert.is_true(health.jump_behavior_valid)
|
||||
assert.is_string(health.plugin_version)
|
||||
end)
|
||||
|
||||
it("should detect invalid configuration in health check", function()
|
||||
-- Manually corrupt the configuration
|
||||
jquote.options.jump_behavior = "invalid"
|
||||
|
||||
local health = jquote.health()
|
||||
assert.is_false(health.jump_behavior_valid)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Edge Cases", function()
|
||||
|
||||
it("should handle empty quote_chars gracefully", function()
|
||||
-- Manually set invalid state to test defensive programming
|
||||
jquote.options.quote_chars = {}
|
||||
mock_vim._set_line_and_cursor("'test'", 2)
|
||||
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success)
|
||||
end)
|
||||
|
||||
it("should handle quote at line boundaries", function()
|
||||
mock_vim._set_line_and_cursor("'start", 0)
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_false(success) -- No closing quote
|
||||
end)
|
||||
|
||||
it("should handle cursor at exact quote position", function()
|
||||
mock_vim._set_line_and_cursor("'test'", 0) -- Cursor on opening quote
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_true(success)
|
||||
assert.are.equal('"test"', test_state.current_line)
|
||||
end)
|
||||
|
||||
it("should handle single character quoted strings", function()
|
||||
mock_vim._set_line_and_cursor("'a'", 1)
|
||||
local success = jquote.toggle_quotes()
|
||||
assert.is_true(success)
|
||||
assert.are.equal('"a"', test_state.current_line)
|
||||
end)
|
||||
end)
|
||||
end)
|
217
tests/test_runner.lua
Normal file
217
tests/test_runner.lua
Normal file
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env lua
|
||||
|
||||
--- Simple Test Runner for JQuote Plugin
|
||||
--- @author Jan
|
||||
--- @license MIT
|
||||
|
||||
local test_runner = {}
|
||||
|
||||
-- Test result tracking
|
||||
local stats = {
|
||||
total = 0,
|
||||
passed = 0,
|
||||
failed = 0,
|
||||
errors = {},
|
||||
start_time = os.clock()
|
||||
}
|
||||
|
||||
-- ANSI color codes for terminal output
|
||||
local colors = {
|
||||
reset = "\27[0m",
|
||||
red = "\27[31m",
|
||||
green = "\27[32m",
|
||||
yellow = "\27[33m",
|
||||
blue = "\27[34m",
|
||||
magenta = "\27[35m",
|
||||
cyan = "\27[36m",
|
||||
white = "\27[37m",
|
||||
bold = "\27[1m"
|
||||
}
|
||||
|
||||
-- Simple assertion library
|
||||
local assert = {
|
||||
is_true = function(value, message)
|
||||
if value ~= true then
|
||||
error(message or string.format("Expected true, got %s", tostring(value)))
|
||||
end
|
||||
end,
|
||||
|
||||
is_false = function(value, message)
|
||||
if value ~= false then
|
||||
error(message or string.format("Expected false, got %s", tostring(value)))
|
||||
end
|
||||
end,
|
||||
|
||||
are = {
|
||||
equal = function(expected, actual, message)
|
||||
if expected ~= actual then
|
||||
error(message or string.format("Expected %s, got %s", tostring(expected), tostring(actual)))
|
||||
end
|
||||
end,
|
||||
same = function(expected, actual, message)
|
||||
if not test_runner.deep_equal(expected, actual) then
|
||||
error(message or string.format("Tables are not the same.\nExpected: %s\nActual: %s",
|
||||
test_runner.table_tostring(expected), test_runner.table_tostring(actual)))
|
||||
end
|
||||
end
|
||||
},
|
||||
|
||||
is_not_nil = function(value, message)
|
||||
if value == nil then
|
||||
error(message or "Expected non-nil value")
|
||||
end
|
||||
end,
|
||||
|
||||
is_string = function(value, message)
|
||||
if type(value) ~= "string" then
|
||||
error(message or string.format("Expected string, got %s", type(value)))
|
||||
end
|
||||
end,
|
||||
|
||||
is_table = function(value, message)
|
||||
if type(value) ~= "table" then
|
||||
error(message or string.format("Expected table, got %s", type(value)))
|
||||
end
|
||||
end,
|
||||
|
||||
matches = function(pattern, str, message)
|
||||
if not string.match(str, pattern) then
|
||||
error(message or string.format("String '%s' does not match pattern '%s'", str, pattern))
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
-- Make assert global for tests
|
||||
_G.assert = assert
|
||||
|
||||
-- Test context management
|
||||
local current_context = ""
|
||||
local current_test = ""
|
||||
|
||||
-- Utility functions
|
||||
function test_runner.deep_equal(a, b)
|
||||
if type(a) ~= type(b) then return false end
|
||||
if type(a) ~= "table" then return a == b end
|
||||
|
||||
for k, v in pairs(a) do
|
||||
if not test_runner.deep_equal(v, b[k]) then return false end
|
||||
end
|
||||
for k, v in pairs(b) do
|
||||
if not test_runner.deep_equal(v, a[k]) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function test_runner.table_tostring(t, indent)
|
||||
if type(t) ~= "table" then return tostring(t) end
|
||||
indent = indent or 0
|
||||
local spaces = string.rep(" ", indent)
|
||||
local result = "{\n"
|
||||
for k, v in pairs(t) do
|
||||
result = result .. spaces .. " " .. tostring(k) .. " = "
|
||||
if type(v) == "table" then
|
||||
result = result .. test_runner.table_tostring(v, indent + 1)
|
||||
else
|
||||
result = result .. tostring(v)
|
||||
end
|
||||
result = result .. ",\n"
|
||||
end
|
||||
result = result .. spaces .. "}"
|
||||
return result
|
||||
end
|
||||
|
||||
-- Test framework functions
|
||||
function describe(description, func)
|
||||
current_context = description
|
||||
print(colors.blue .. colors.bold .. "\n" .. description .. colors.reset)
|
||||
func()
|
||||
current_context = ""
|
||||
end
|
||||
|
||||
function it(description, func)
|
||||
current_test = description
|
||||
stats.total = stats.total + 1
|
||||
|
||||
local success, error_msg = pcall(func)
|
||||
|
||||
if success then
|
||||
stats.passed = stats.passed + 1
|
||||
print(colors.green .. " ✓ " .. description .. colors.reset)
|
||||
else
|
||||
stats.failed = stats.failed + 1
|
||||
local full_description = current_context .. " - " .. description
|
||||
table.insert(stats.errors, {
|
||||
test = full_description,
|
||||
error = error_msg
|
||||
})
|
||||
print(colors.red .. " ✗ " .. description .. colors.reset)
|
||||
print(colors.red .. " " .. error_msg .. colors.reset)
|
||||
end
|
||||
|
||||
current_test = ""
|
||||
end
|
||||
|
||||
function before_each(func)
|
||||
-- Simple implementation - just call the function before each test
|
||||
-- In a more sophisticated runner, this would be stored and called appropriately
|
||||
_G._before_each = func
|
||||
end
|
||||
|
||||
-- Override 'it' to call before_each if it exists
|
||||
local original_it = it
|
||||
function it(description, func)
|
||||
if _G._before_each then
|
||||
_G._before_each()
|
||||
end
|
||||
original_it(description, func)
|
||||
end
|
||||
|
||||
-- Test execution
|
||||
function test_runner.run_tests()
|
||||
print(colors.cyan .. colors.bold .. "JQuote Plugin Test Suite" .. colors.reset)
|
||||
print(colors.cyan .. "========================" .. colors.reset)
|
||||
|
||||
-- Load and run tests
|
||||
local success, error_msg = pcall(dofile, "tests/init_spec.lua")
|
||||
|
||||
if not success then
|
||||
print(colors.red .. colors.bold .. "Failed to load test file:" .. colors.reset)
|
||||
print(colors.red .. error_msg .. colors.reset)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Print summary
|
||||
local duration = os.clock() - stats.start_time
|
||||
print(colors.cyan .. "\n========================" .. colors.reset)
|
||||
print(colors.white .. colors.bold .. "Test Summary:" .. colors.reset)
|
||||
print(string.format(" Total tests: %d", stats.total))
|
||||
print(colors.green .. string.format(" Passed: %d", stats.passed) .. colors.reset)
|
||||
|
||||
if stats.failed > 0 then
|
||||
print(colors.red .. string.format(" Failed: %d", stats.failed) .. colors.reset)
|
||||
print(colors.red .. "\nFailure Details:" .. colors.reset)
|
||||
for _, error_info in ipairs(stats.errors) do
|
||||
print(colors.red .. " • " .. error_info.test .. colors.reset)
|
||||
print(colors.red .. " " .. error_info.error .. colors.reset)
|
||||
end
|
||||
end
|
||||
|
||||
print(string.format(" Duration: %.3fs", duration))
|
||||
|
||||
local success_rate = stats.total > 0 and (stats.passed / stats.total * 100) or 0
|
||||
if success_rate == 100 then
|
||||
print(colors.green .. colors.bold .. "\n🎉 All tests passed!" .. colors.reset)
|
||||
else
|
||||
print(colors.red .. colors.bold .. string.format("\n❌ %.1f%% tests passed", success_rate) .. colors.reset)
|
||||
end
|
||||
|
||||
return stats.failed == 0
|
||||
end
|
||||
|
||||
-- Command line interface
|
||||
if arg and arg[0] and arg[0]:match("test_runner%.lua$") then
|
||||
local success = test_runner.run_tests()
|
||||
os.exit(success and 0 or 1)
|
||||
end
|
||||
|
||||
return test_runner
|
Loading…
Add table
Add a link
Reference in a new issue