aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteven Arcangeli <stevearc@stevearc.com>2023-08-27 18:15:04 -0700
committerSteven Arcangeli <stevearc@stevearc.com>2023-08-27 18:15:09 -0700
commitf87f3ea322b1111e1929d149224ff736c8390db3 (patch)
tree44b11765d02fbb8b88bedafa4bde69ff33d22ad9
parent446aa570048586f9c13f1ea88e280567f336691e (diff)
test: add a test suite
-rw-r--r--.github/workflows/tests.yml23
-rw-r--r--.gitignore2
-rw-r--r--lua/conform/init.lua2
-rw-r--r--lua/conform/runner.lua37
-rwxr-xr-xrun_tests.sh24
-rwxr-xr-xtests/fake_formatter.sh17
-rw-r--r--tests/minimal_init.lua5
-rw-r--r--tests/runner_spec.lua243
-rw-r--r--tests/test_util.lua34
9 files changed, 376 insertions, 11 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ca71ef2..7e5b6e0 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -40,6 +40,28 @@ jobs:
version: v0.15.2
args: --check .
+ run_tests:
+ strategy:
+ matrix:
+ include:
+ - nvim_tag: v0.8.3
+ - nvim_tag: v0.9.1
+
+ name: Run tests
+ runs-on: ubuntu-22.04
+ env:
+ NVIM_TAG: ${{ matrix.nvim_tag }}
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Neovim and dependencies
+ run: |
+ bash ./.github/workflows/install_nvim.sh
+
+ - name: Run tests
+ run: |
+ bash ./run_tests.sh
+
release:
name: release
@@ -48,6 +70,7 @@ jobs:
- luacheck
- stylua
- typecheck
+ - run_tests
runs-on: ubuntu-22.04
steps:
- uses: google-github-actions/release-please-action@v3
diff --git a/.gitignore b/.gitignore
index d818abb..b29818a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,5 @@ luac.out
.direnv/
.testenv/
doc/tags
+tests/testfile.txt
+tests/fake_formatter_output
diff --git a/lua/conform/init.lua b/lua/conform/init.lua
index 15c4317..5b8bd5b 100644
--- a/lua/conform/init.lua
+++ b/lua/conform/init.lua
@@ -174,7 +174,7 @@ M.list_formatters = function(bufnr)
for formatter in pairs(formatters) do
local info = M.get_formatter_info(formatter)
if info.available then
- table.insert(all_info, assert(info))
+ table.insert(all_info, info)
if not run_options.run_all_formatters then
break
end
diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua
index 83647a2..4c8fea7 100644
--- a/lua/conform/runner.lua
+++ b/lua/conform/runner.lua
@@ -6,7 +6,7 @@ local M = {}
---@param ctx conform.Context
---@param config conform.FormatterConfig
-local function build_cmd(ctx, config)
+M.build_cmd = function(ctx, config)
local command = config.command
if type(command) == "function" then
command = command(ctx)
@@ -22,7 +22,7 @@ local function build_cmd(ctx, config)
if v == "$FILENAME" then
v = ctx.filename
elseif v == "$DIRNAME" then
- v = vim.fs.dirname(ctx.filename)
+ v = ctx.dirname
end
table.insert(cmd, v)
end
@@ -37,23 +37,38 @@ local function apply_format(bufnr, original_lines, new_lines)
local restore = util.save_win_positions(bufnr)
local original_text = table.concat(original_lines, "\n")
- -- Trim off the final newline because the original lines won't have it
- -- and we want the diffs to agree if the file is unchanged
+ -- Trim off the final newline from the formatted text because that is baked in to
+ -- the vim lines representation
if new_lines[#new_lines] == "" then
new_lines[#new_lines] = nil
end
local new_text = table.concat(new_lines, "\n")
local indices = vim.diff(original_text, new_text, {
result_type = "indices",
- algorithm = "minimal",
+ algorithm = "histogram",
})
assert(indices)
for i = #indices, 1, -1 do
local start_a, count_a, start_b, count_b = unpack(indices[i])
-- When count_a is 0, the diff is an insert after the line
if count_a == 0 then
+ -- This happens when the first line is blank and we're inserting text after it
+ if start_a == 0 then
+ count_a = 1
+ end
start_a = start_a + 1
end
+
+ -- If this diff range goes *up to* the last line in the original file, *and* the last line
+ -- after that is just an empty space, then the diff range here was calculated to include that
+ -- final newline, so we should bump up the count_a to include it
+ if (start_a + count_a) == #original_lines and original_lines[#original_lines] == "" then
+ count_a = count_a + 1
+ end
+ -- Same logic for the new lines
+ if (start_b + count_b) == #new_lines and new_lines[#new_lines] == "" then
+ count_b = count_b + 1
+ end
local replacement = util.tbl_slice(new_lines, start_b, start_b + count_b - 1)
vim.api.nvim_buf_set_lines(bufnr, start_a - 1, start_a - 1 + count_a, true, replacement)
end
@@ -69,7 +84,7 @@ end
local function run_formatter(bufnr, formatter, input_lines, callback)
local config = assert(require("conform").get_formatter_config(formatter.name))
local ctx = M.build_context(bufnr, config)
- local cmd = build_cmd(ctx, config)
+ local cmd = M.build_cmd(ctx, config)
local cwd = nil
if config.cwd then
cwd = config.cwd(ctx)
@@ -125,9 +140,11 @@ local function run_formatter(bufnr, formatter, input_lines, callback)
log.error("Formatter %s exited with code %d", formatter.name, code)
log.warn("Formatter %s stdout:", formatter.name, stdout)
log.warn("Formatter %s stderr:", formatter.name, stderr)
- callback(
- string.format("Formatter '%s' error: %s", formatter.name, table.concat(stderr, "\n"))
- )
+ local stderr_str
+ if stderr then
+ stderr_str = table.concat(stderr, "\n")
+ end
+ callback(string.format("Formatter '%s' error: %s", formatter.name, stderr_str))
end
end,
})
@@ -223,7 +240,7 @@ M.format_async = function(bufnr, formatters, callback)
jid = run_formatter(bufnr, formatter, input_lines, function(err, output)
if err then
-- Only display the error if the job wasn't canceled
- if jid == vim.b[bufnr].conform_jid then
+ if vim.api.nvim_buf_is_valid(bufnr) and jid == vim.b[bufnr].conform_jid then
vim.notify(err, vim.log.levels.ERROR)
end
if callback then
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 0000000..98b4fa7
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+set -e
+
+mkdir -p ".testenv/config/nvim"
+mkdir -p ".testenv/data/nvim"
+mkdir -p ".testenv/state/nvim"
+mkdir -p ".testenv/run/nvim"
+mkdir -p ".testenv/cache/nvim"
+PLUGINS=".testenv/data/nvim/site/pack/plugins/start"
+
+if [ ! -e "$PLUGINS/plenary.nvim" ]; then
+ git clone --depth=1 https://github.com/nvim-lua/plenary.nvim.git "$PLUGINS/plenary.nvim"
+else
+ (cd "$PLUGINS/plenary.nvim" && git pull)
+fi
+
+XDG_CONFIG_HOME=".testenv/config" \
+ XDG_DATA_HOME=".testenv/data" \
+ XDG_STATE_HOME=".testenv/state" \
+ XDG_RUNTIME_DIR=".testenv/run" \
+ XDG_CACHE_HOME=".testenv/cache" \
+ nvim --headless -u tests/minimal_init.lua \
+ -c "PlenaryBustedDirectory ${1-tests} { minimal_init = './tests/minimal_init.lua' }"
+echo "Success"
diff --git a/tests/fake_formatter.sh b/tests/fake_formatter.sh
new file mode 100755
index 0000000..a060a4c
--- /dev/null
+++ b/tests/fake_formatter.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -e
+
+if [ -e "tests/fake_formatter_output" ]; then
+ cat tests/fake_formatter_output
+else
+ cat
+fi
+
+if [ "$1" = "--fail" ]; then
+ echo "failure" >&2
+ exit 1
+elif [ "$1" = "--timeout" ]; then
+ sleep 4
+fi
+
diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua
new file mode 100644
index 0000000..262d9ec
--- /dev/null
+++ b/tests/minimal_init.lua
@@ -0,0 +1,5 @@
+vim.cmd([[set runtimepath+=.]])
+
+vim.o.swapfile = false
+vim.bo.swapfile = false
+require("tests.test_util").reset_editor()
diff --git a/tests/runner_spec.lua b/tests/runner_spec.lua
new file mode 100644
index 0000000..016c8a4
--- /dev/null
+++ b/tests/runner_spec.lua
@@ -0,0 +1,243 @@
+require("plenary.async").tests.add_to_env()
+local test_util = require("tests.test_util")
+local conform = require("conform")
+local runner = require("conform.runner")
+
+describe("runner", function()
+ after_each(function()
+ test_util.reset_editor()
+ end)
+
+ it("resolves config function", function()
+ conform.formatters.test = function()
+ return {
+ meta = { url = "", description = "" },
+ command = "echo",
+ }
+ end
+ local config = assert(conform.get_formatter_config("test"))
+ assert.are.same({
+ meta = { url = "", description = "" },
+ command = "echo",
+ stdin = true,
+ }, config)
+ end)
+
+ describe("build_context", function()
+ it("sets the filename and dirname", function()
+ vim.cmd.edit({ args = { "README.md" } })
+ local bufnr = vim.api.nvim_get_current_buf()
+ conform.formatters.test = {
+ meta = { url = "", description = "" },
+ command = "echo",
+ }
+ local config = assert(conform.get_formatter_config("test"))
+ local ctx = runner.build_context(0, config)
+ local filename = vim.api.nvim_buf_get_name(bufnr)
+ assert.are.same({
+ buf = bufnr,
+ filename = filename,
+ dirname = vim.fs.dirname(filename),
+ }, ctx)
+ end)
+
+ it("sets temp file when stdin = false", function()
+ vim.cmd.edit({ args = { "README.md" } })
+ local bufnr = vim.api.nvim_get_current_buf()
+ conform.formatters.test = {
+ meta = { url = "", description = "" },
+ command = "echo",
+ stdin = false,
+ }
+ local config = assert(conform.get_formatter_config("test"))
+ local ctx = runner.build_context(0, config)
+ local bufname = vim.api.nvim_buf_get_name(bufnr)
+ local dirname = vim.fs.dirname(bufname)
+ assert.equal(bufnr, ctx.buf)
+ assert.equal(dirname, ctx.dirname)
+ assert.truthy(ctx.filename:match(dirname .. "/.conform.%d+.README.md$"))
+ end)
+ end)
+
+ describe("build_cmd", function()
+ it("replaces $FILENAME in args", function()
+ vim.cmd.edit({ args = { "README.md" } })
+ local bufnr = vim.api.nvim_get_current_buf()
+ conform.formatters.test = {
+ meta = { url = "", description = "" },
+ command = "echo",
+ args = { "$FILENAME" },
+ }
+ local config = assert(conform.get_formatter_config("test"))
+ local ctx = runner.build_context(0, config)
+ local cmd = runner.build_cmd(ctx, config)
+ assert.are.same({ "echo", vim.api.nvim_buf_get_name(bufnr) }, cmd)
+ end)
+
+ it("replaces $DIRNAME in args", function()
+ vim.cmd.edit({ args = { "README.md" } })
+ local bufnr = vim.api.nvim_get_current_buf()
+ conform.formatters.test = {
+ meta = { url = "", description = "" },
+ command = "echo",
+ args = { "$DIRNAME" },
+ }
+ local config = assert(conform.get_formatter_config("test"))
+ local ctx = runner.build_context(0, config)
+ local cmd = runner.build_cmd(ctx, config)
+ assert.are.same({ "echo", vim.fs.dirname(vim.api.nvim_buf_get_name(bufnr)) }, cmd)
+ end)
+
+ it("resolves arg function", function()
+ vim.cmd.edit({ args = { "README.md" } })
+ local bufnr = vim.api.nvim_get_current_buf()
+ conform.formatters.test = {
+ meta = { url = "", description = "" },
+ command = "echo",
+ args = function()
+ return { "--stdin" }
+ end,
+ }
+ local config = assert(conform.get_formatter_config("test"))
+ local ctx = runner.build_context(0, config)
+ local cmd = runner.build_cmd(ctx, config)
+ assert.are.same({ "echo", "--stdin" }, cmd)
+ end)
+ end)
+
+ describe("e2e", function()
+ before_each(function()
+ conform.formatters.test = {
+ meta = { url = "", description = "" },
+ command = "tests/fake_formatter.sh",
+ }
+ end)
+
+ ---@param buf_content string
+ ---@param expected string
+ ---@param opts? table
+ local function run_formatter(buf_content, expected, opts)
+ local bufnr = vim.fn.bufadd("testfile")
+ vim.fn.bufload(bufnr)
+ vim.api.nvim_set_current_buf(bufnr)
+ local lines = vim.split(buf_content, "\n", { plain = true })
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
+ vim.bo[bufnr].modified = false
+ local expected_lines = vim.split(expected, "\n", { plain = true })
+ test_util.set_formatter_output(expected_lines)
+ conform.format(vim.tbl_extend("force", opts or {}, { formatters = { "test" } }))
+ return expected_lines
+ end
+
+ ---@param buf_content string
+ ---@param new_content string
+ ---@param expected? string[]
+ local function run_formatter_test(buf_content, new_content, expected)
+ local lines = run_formatter(buf_content, new_content)
+ assert.are.same(expected or lines, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end
+
+ it("sets the correct output", function()
+ run_formatter_test(
+ [[
+ if true {
+ print("hello")
+ }]],
+ [[
+ if true {
+ print("hello")
+ }]]
+ )
+ run_formatter_test(
+ [[
+ if true {
+ print("hello")
+ }]],
+ [[
+ if true {
+ print("goodbye")
+ }]]
+ )
+ run_formatter_test(
+ [[
+ if true {
+ print("hello")
+ }]],
+ [[
+ if true {
+ print("hello world")
+ print("hello world")
+ print("hello world")
+ }]]
+ )
+ run_formatter_test(
+ [[
+print("a")
+print("b")
+print("c")
+ ]],
+ [[
+print("c")
+print("b")
+print("a")
+ ]]
+ )
+ run_formatter_test("hello\ngoodbye", "hello\n\n\ngoodbye", { "hello", "", "", "goodbye" })
+ run_formatter_test("hello", "hello\ngoodbye", { "hello", "goodbye" })
+ run_formatter_test("", "hello", { "hello" })
+ run_formatter_test("\nfoo", "\nhello\nfoo", { "", "hello", "foo" })
+ run_formatter_test("hello", "hello\n\n", { "hello", "" })
+ run_formatter_test("hello", "hello\n", { "hello" })
+ assert.falsy(vim.bo.modified)
+ run_formatter_test("hello\n", "hello", { "hello" })
+ run_formatter_test("hello\n ", "hello", { "hello" })
+ end)
+
+ it("does not change output if formatter fails", function()
+ conform.formatters.test.args = { "--fail" }
+ run_formatter("hello", "goodbye")
+ assert.are.same({ "hello" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end)
+
+ it("allows nonzero exit codes", function()
+ conform.formatters.test.args = { "--fail" }
+ conform.formatters.test.exit_codes = { 0, 1 }
+ run_formatter_test("hello", "goodbye")
+ end)
+
+ it("does not format if it times out", function()
+ conform.formatters.test.args = { "--timeout" }
+ run_formatter("hello", "goodbye", { timeout_ms = 10 })
+ assert.are.same({ "hello" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end)
+
+ it("can format async", function()
+ run_formatter("hello", "goodbye", { async = true })
+ assert.are.same({ "hello" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ vim.wait(100)
+ assert.are.same({ "goodbye" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end)
+
+ it("discards formatting changes if buffer has been concurrently modified", function()
+ run_formatter("hello", "goodbye", { async = true })
+ assert.are.same({ "hello" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ vim.api.nvim_buf_set_lines(0, 0, -1, true, { "newcontent" })
+ vim.wait(100)
+ assert.are.same({ "newcontent" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end)
+
+ it("formats on save", function()
+ conform.setup({
+ formatters_by_ft = { ["*"] = { "test" } },
+ format_on_save = true,
+ })
+ vim.cmd.edit({ args = { "tests/testfile.txt" } })
+ vim.api.nvim_buf_set_lines(0, 0, -1, true, { "hello" })
+ test_util.set_formatter_output({ "goodbye" })
+ vim.cmd.write()
+ local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
+ vim.fn.delete("tests/testfile.txt")
+ assert.are.same({ "goodbye" }, lines)
+ end)
+ end)
+end)
diff --git a/tests/test_util.lua b/tests/test_util.lua
new file mode 100644
index 0000000..a225aec
--- /dev/null
+++ b/tests/test_util.lua
@@ -0,0 +1,34 @@
+require("plenary.async").tests.add_to_env()
+local conform = require("conform")
+local M = {}
+
+local OUTPUT_FILE = "tests/fake_formatter_output"
+
+M.reset_editor = function()
+ vim.cmd.tabonly({ mods = { silent = true } })
+ for i, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
+ if i > 1 then
+ vim.api.nvim_win_close(winid, true)
+ end
+ end
+ vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(false, true))
+ for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
+ vim.api.nvim_buf_delete(bufnr, { force = true })
+ end
+ conform.formatters = {}
+ conform.formatters_by_ft = {}
+ pcall(vim.api.nvim_del_augroup_by_name, "Conform")
+ if vim.fn.filereadable(OUTPUT_FILE) == 1 then
+ vim.fn.delete(OUTPUT_FILE)
+ end
+end
+
+---@param lines string[]
+M.set_formatter_output = function(lines)
+ local content = table.concat(lines, "\n")
+ local fd = assert(vim.loop.fs_open(OUTPUT_FILE, "w", 420)) -- 0644
+ vim.loop.fs_write(fd, content)
+ vim.loop.fs_close(fd)
+end
+
+return M