mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 10:46:08 +00:00
1009 lines
31 KiB
Lua
Executable File
1009 lines
31 KiB
Lua
Executable File
#!/usr/bin/env -S nvim -l
|
||
|
||
-- Simple test runner using Neovim's built-in features
|
||
-- Run with: nvim -l tests/run_tests.lua
|
||
|
||
-- Add lua directory to package path
|
||
package.path = package.path .. ";lua/?.lua;lua/?/init.lua"
|
||
|
||
-- Load the parser module
|
||
local parser = require("jj.core.parser")
|
||
|
||
local utils = require("jj.utils")
|
||
local jj_args = require("jj.core.args")
|
||
|
||
local tests_passed = 0
|
||
local tests_failed = 0
|
||
local failures = {}
|
||
|
||
local function assert_equals(expected, actual, msg)
|
||
if expected ~= actual then
|
||
error(
|
||
string.format("%s\nExpected: %s\nGot: %s", msg or "Assertion failed", tostring(expected), tostring(actual))
|
||
)
|
||
end
|
||
end
|
||
|
||
local function assert_is_nil(value, msg)
|
||
if value ~= nil then
|
||
error(string.format("%s\nExpected: nil\nGot: %s", msg or "Assertion failed", tostring(value)))
|
||
end
|
||
end
|
||
|
||
local function assert_table_equals(expected, actual, msg)
|
||
if type(expected) ~= "table" or type(actual) ~= "table" then
|
||
error(
|
||
string.format("%s\nExpected table, got: %s and %s", msg or "Assertion failed", type(expected), type(actual))
|
||
)
|
||
end
|
||
if not vim.deep_equal(expected, actual) then
|
||
error(
|
||
string.format(
|
||
"%s\nExpected: %s\nGot: %s",
|
||
msg or "Assertion failed",
|
||
vim.inspect(expected),
|
||
vim.inspect(actual)
|
||
)
|
||
)
|
||
end
|
||
end
|
||
|
||
local function run_test(name, test_fn)
|
||
local status, err = pcall(test_fn)
|
||
if status then
|
||
tests_passed = tests_passed + 1
|
||
print(string.format("✓ %s", name))
|
||
else
|
||
tests_failed = tests_failed + 1
|
||
print(string.format("✗ %s", name))
|
||
table.insert(failures, { name = name, error = err })
|
||
end
|
||
end
|
||
|
||
print("\n=== Running parser tests ===\n")
|
||
|
||
-- Test cases
|
||
run_test("parses simple diamond symbol", function()
|
||
local line = "◆ abc123 my commit message"
|
||
assert_equals("abc123", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses simple circle symbol", function()
|
||
local line = "○ def456 another commit"
|
||
assert_equals("def456", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses simple @ symbol", function()
|
||
local line = "@ def456 another commit"
|
||
assert_equals("def456", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses conflict symbol", function()
|
||
local line = "× ghi789 conflicted commit"
|
||
assert_equals("ghi789", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with leading whitespace", function()
|
||
local line = " ◆ jkl012 indented commit"
|
||
assert_equals("jkl012", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses single branch with box drawing", function()
|
||
local line = "│ ○ mno345 commit on branch"
|
||
assert_equals("mno345", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses multiple branches", function()
|
||
local line = "│ │ ◆ pqr678 commit with multiple branches"
|
||
assert_equals("pqr678", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses complex graph with connectors", function()
|
||
local line = "├─○ stu901 commit after merge"
|
||
assert_equals("stu901", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses graph with multiple box chars", function()
|
||
local line = "│ ├─◆ vwx234 complex branch"
|
||
assert_equals("vwx234", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses ASCII @ symbol", function()
|
||
local line = "@ yza567 current working copy"
|
||
assert_equals("yza567", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses ASCII * symbol (git-style)", function()
|
||
local line = "* bcd890 git style commit"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses ASCII graph with pipe", function()
|
||
local line = "| * efg123 ascii branch"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses mixed ASCII graph", function()
|
||
local line = "|\\ @ hij456 merge commit"
|
||
assert_equals("hij456", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses @ not at top", function()
|
||
local line = "│ @ klm789 current change in middle"
|
||
assert_equals("klm789", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses @ with multiple branches", function()
|
||
local line = "│ │ @ nop012 working copy on branch"
|
||
assert_equals("nop012", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses @ after merge connector", function()
|
||
local line = "├─@ qrs345 working copy after merge"
|
||
assert_equals("qrs345", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with various box drawing characters", function()
|
||
local line = "╭─╮ ○ tuv678 fancy box"
|
||
assert_equals("tuv678", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses deeply nested branches", function()
|
||
local line = "│ │ │ │ ◆ wxy901 deeply nested"
|
||
assert_equals("wxy901", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses revision with numbers and letters", function()
|
||
local line = "◆ abc123def456 mixed alphanumeric"
|
||
assert_equals("abc123def456", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("stops at first non-alphanumeric after revision", function()
|
||
local line = "○ xyz789 this is the message"
|
||
assert_equals("xyz789", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with different line connector styles", function()
|
||
local line = "┼─┤ ◆ zab234 cross connector"
|
||
assert_equals("zab234", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with curve connectors", function()
|
||
local line = "╰─○ cde567 curve connector"
|
||
assert_equals("cde567", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with double line vertical ┃", function()
|
||
local line = "┃ ◆ fgh890 double line vertical"
|
||
assert_equals("fgh890", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with light triple dash vertical ┆", function()
|
||
local line = "┆ ○ ijk123 light triple dash"
|
||
assert_equals("ijk123", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with heavy triple dash vertical ┇", function()
|
||
local line = "┇ ◆ lmn456 heavy triple dash"
|
||
assert_equals("lmn456", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with light quadruple dash vertical ┊", function()
|
||
local line = "┊ ○ opq789 light quadruple dash"
|
||
assert_equals("opq789", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with heavy quadruple dash vertical ┋", function()
|
||
local line = "┋ ◆ rst012 heavy quadruple dash"
|
||
assert_equals("rst012", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with top-left corner ┌", function()
|
||
local line = "┌─○ uvw345 top left corner"
|
||
assert_equals("uvw345", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with top-right corner ┐", function()
|
||
local line = "┐ ◆ xyz678 top right corner"
|
||
assert_equals("xyz678", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with bottom-left corner └", function()
|
||
local line = "└─○ abc901 bottom left corner"
|
||
assert_equals("abc901", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with bottom-right corner ┘", function()
|
||
local line = "┘ ◆ def234 bottom right corner"
|
||
assert_equals("def234", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with left tee ├", function()
|
||
local line = "├ ○ ghi567 left tee"
|
||
assert_equals("ghi567", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with right tee ┤", function()
|
||
local line = "┤ ◆ jkl890 right tee"
|
||
assert_equals("jkl890", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with top tee ┬", function()
|
||
local line = "┬─○ mno123 top tee"
|
||
assert_equals("mno123", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with bottom tee ┴", function()
|
||
local line = "┴─◆ pqr456 bottom tee"
|
||
assert_equals("pqr456", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with cross ┼", function()
|
||
local line = "┼ ○ stu789 cross"
|
||
assert_equals("stu789", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with mixed special box chars", function()
|
||
local line = "┃ ┆ ┇ ○ vwx012 mixed special"
|
||
assert_equals("vwx012", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses with rounded corners", function()
|
||
local line = "╭─╮ ╰─╯ ◆ yza345 rounded corners"
|
||
assert_equals("yza345", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for lines without revision", function()
|
||
local line = "This is just a description line"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for empty line", function()
|
||
local line = ""
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for only graph characters", function()
|
||
local line = "│ │ ├─"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
-- Regression tests for false positives with description lines
|
||
run_test("returns nil for description line 'go' (false positive)", function()
|
||
local line = "│ │ go mod tidy"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for description line 'Improve' (false positive)", function()
|
||
local line = "│ │ │ Improve input validation and UX for repository"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for description line 'Add' (false positive)", function()
|
||
local line = "│ │ │ Add Makefile and performance docs"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for description line starting with word (graph only)", function()
|
||
local line = "├─── description text here"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("returns nil for line with only graph chars and text", function()
|
||
local line = "│ │ │ commit message without symbol"
|
||
assert_is_nil(parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("still parses correctly when symbol is present", function()
|
||
local line = "│ │ ◆ s some description"
|
||
assert_equals("s", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("still parses correctly with box chars and symbol", function()
|
||
local line = "├─○ go some description"
|
||
assert_equals("go", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("still parses correctly with divergent change suffix /0", function()
|
||
local line = "├─○ go/0 some description"
|
||
assert_equals("go/0", parser.get_revset(line))
|
||
end)
|
||
|
||
-- Divergent changes tests
|
||
run_test("parses divergent change with /0", function()
|
||
local line = "◆ abc123/0 first divergent copy"
|
||
assert_equals("abc123/0", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with /1", function()
|
||
local line = "○ rs/1 lam@lamtrung.com 9 hours ago pr-83-1 6a7 (divergent)"
|
||
assert_equals("rs/1", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with /2", function()
|
||
local line = "◆ ghi789/2 third divergent copy"
|
||
assert_equals("ghi789/2", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with higher number /10", function()
|
||
local line = "○ jkl012/10 tenth divergent copy"
|
||
assert_equals("jkl012/10", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with graph chars", function()
|
||
local line = "│ │ ◆ mno345/0 divergent on branch"
|
||
assert_equals("mno345/0", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with @ symbol", function()
|
||
local line = "@ pqr678/1 working copy divergent"
|
||
assert_equals("pqr678/1", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with conflict symbol", function()
|
||
local line = "× stu901/0 conflicted divergent"
|
||
assert_equals("stu901/0", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change with merge connector", function()
|
||
local line = "├─○ vwx234/2 divergent after merge"
|
||
assert_equals("vwx234/2", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change deeply nested", function()
|
||
local line = "│ │ │ │ ◆ yza567/0 deeply nested divergent"
|
||
assert_equals("yza567/0", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses short revset with divergent suffix", function()
|
||
local line = "○ a/0 single char divergent"
|
||
assert_equals("a/0", parser.get_revset(line))
|
||
end)
|
||
|
||
run_test("parses divergent change at end of line", function()
|
||
local line = "◆ bcd890/1"
|
||
assert_equals("bcd890/1", parser.get_revset(line))
|
||
end)
|
||
|
||
print("\n=== Running parse_conflicted_files tests ===\n")
|
||
|
||
run_test("parse_conflicted_files: single file", function()
|
||
assert_table_equals(
|
||
{ { rel_path = "src/foo.rs", abs_path = "/repo/src/foo.rs" } },
|
||
parser.parse_conflicted_files("src/foo.rs\0/repo/src/foo.rs\0")
|
||
)
|
||
end)
|
||
|
||
run_test("parse_conflicted_files: multiple files", function()
|
||
assert_table_equals({
|
||
{ rel_path = "a.txt", abs_path = "/repo/a.txt" },
|
||
{ rel_path = "b.txt", abs_path = "/repo/b.txt" },
|
||
}, parser.parse_conflicted_files("a.txt\0/repo/a.txt\0b.txt\0/repo/b.txt\0"))
|
||
end)
|
||
|
||
run_test("parse_conflicted_files: paths with tabs and newlines", function()
|
||
assert_table_equals(
|
||
{ { rel_path = "weird\tname\n.txt", abs_path = "/repo/weird\tname\n.txt" } },
|
||
parser.parse_conflicted_files("weird\tname\n.txt\0/repo/weird\tname\n.txt\0")
|
||
)
|
||
end)
|
||
|
||
run_test("parse_conflicted_files: empty output yields empty list", function()
|
||
assert_table_equals({}, parser.parse_conflicted_files(""))
|
||
end)
|
||
|
||
run_test("parse_conflicted_files: nil output yields empty list", function()
|
||
assert_table_equals({}, parser.parse_conflicted_files(nil))
|
||
end)
|
||
|
||
print("\n=== Running scan_conflict_sections tests ===\n")
|
||
|
||
run_test("scan_conflict_sections: one entry per opening marker", function()
|
||
local lines = {
|
||
"line 1",
|
||
"<<<<<<< Conflict 1 of 2",
|
||
"%%%%%%%",
|
||
">>>>>>>",
|
||
"between",
|
||
"<<<<<<< Conflict 2 of 2",
|
||
">>>>>>>",
|
||
}
|
||
assert_table_equals({
|
||
{
|
||
file = "/abs/a.txt",
|
||
rel_path = "a.txt",
|
||
pos = { 2, 0 },
|
||
text = "a.txt:2",
|
||
},
|
||
{
|
||
file = "/abs/a.txt",
|
||
rel_path = "a.txt",
|
||
pos = { 6, 0 },
|
||
text = "a.txt:6",
|
||
},
|
||
}, parser.scan_conflict_sections("a.txt", "/abs/a.txt", lines))
|
||
end)
|
||
|
||
run_test("scan_conflict_sections: no markers yields empty list", function()
|
||
assert_table_equals({}, parser.scan_conflict_sections("a.txt", "/abs/a.txt", { "no", "markers", "here" }))
|
||
end)
|
||
|
||
run_test("scan_conflict_sections: empty file yields empty list", function()
|
||
assert_table_equals({}, parser.scan_conflict_sections("a.txt", "/abs/a.txt", {}))
|
||
end)
|
||
|
||
print("\n=== Running parse_file_info_from_status_line tests ===\n")
|
||
|
||
run_test("status line: parses regular modified file", function()
|
||
assert_table_equals(
|
||
{ old_path = "src/main.rs", new_path = "src/main.rs", is_rename = false },
|
||
parser.parse_file_info_from_status_line("M src/main.rs")
|
||
)
|
||
end)
|
||
|
||
run_test("status line: parses top-level brace rename", function()
|
||
assert_table_equals(
|
||
{ old_path = "old.txt", new_path = "new.txt", is_rename = true },
|
||
parser.parse_file_info_from_status_line("R {old.txt => new.txt}")
|
||
)
|
||
end)
|
||
|
||
run_test("status line: parses rename with brace group at end", function()
|
||
assert_table_equals(
|
||
{ old_path = "dir/old.txt", new_path = "dir/new.txt", is_rename = true },
|
||
parser.parse_file_info_from_status_line("R dir/{old.txt => new.txt}")
|
||
)
|
||
end)
|
||
|
||
run_test("status line: parses rename with brace group mid-path", function()
|
||
assert_table_equals(
|
||
{
|
||
old_path = "Business/OAuth/Exceptions/OAuth2RefreshTokenExpiredException.php",
|
||
new_path = "Business/OAuth2/Exceptions/OAuth2RefreshTokenExpiredException.php",
|
||
is_rename = true,
|
||
},
|
||
parser.parse_file_info_from_status_line(
|
||
"R Business/{OAuth => OAuth2}/Exceptions/OAuth2RefreshTokenExpiredException.php"
|
||
)
|
||
)
|
||
end)
|
||
|
||
run_test("status line: parses rename with empty old side mid-path", function()
|
||
assert_table_equals({
|
||
old_path = "Business/Exceptions/Error.php",
|
||
new_path = "Business/OAuth2/Sub/Exceptions/Error.php",
|
||
is_rename = true,
|
||
}, parser.parse_file_info_from_status_line("R Business/{ => OAuth2/Sub}/Exceptions/Error.php"))
|
||
end)
|
||
|
||
run_test("status line: parses rename with empty new side mid-path", function()
|
||
assert_table_equals({
|
||
old_path = "Business/OAuth/Exceptions/Error.php",
|
||
new_path = "Business/Exceptions/Error.php",
|
||
is_rename = true,
|
||
}, parser.parse_file_info_from_status_line("R Business/{OAuth => }/Exceptions/Error.php"))
|
||
end)
|
||
|
||
run_test("status line: parses rename with empty side at top level", function()
|
||
assert_table_equals(
|
||
{ old_path = "a.txt", new_path = "dir/a.txt", is_rename = true },
|
||
parser.parse_file_info_from_status_line("R { => dir}/a.txt")
|
||
)
|
||
end)
|
||
|
||
run_test("status line: parses copied file with brace group", function()
|
||
assert_table_equals(
|
||
{ old_path = "dir/old.txt", new_path = "dir/new.txt", is_rename = true },
|
||
parser.parse_file_info_from_status_line("C dir/{old.txt => new.txt}")
|
||
)
|
||
end)
|
||
|
||
print("\n=== Running parse_default_cmd tests ===\n")
|
||
|
||
run_test("parse_default_cmd: parses config list array output", function()
|
||
local output = '["log", "--no-pager", "--limit", "18"]'
|
||
assert_table_equals({ "log", "--no-pager", "--limit", "18" }, parser.parse_json_command(output))
|
||
end)
|
||
|
||
run_test("parse_default_cmd: parses bare array (config get format)", function()
|
||
local output = '["log", "--limit", "10"]'
|
||
assert_table_equals({ "log", "--limit", "10" }, parser.parse_json_command(output))
|
||
end)
|
||
|
||
run_test("parse_default_cmd: parses bare string (config get format)", function()
|
||
local output = '"log"'
|
||
assert_table_equals({ "log" }, parser.parse_json_command(output))
|
||
end)
|
||
|
||
run_test("parse_default_cmd: parses unquoted string", function()
|
||
local output = "log"
|
||
assert_is_nil(parser.parse_json_command(output))
|
||
end)
|
||
|
||
run_test("parse_default_cmd: returns nil for empty string", function()
|
||
assert_is_nil(parser.parse_json_command(""))
|
||
end)
|
||
|
||
run_test("parse_default_cmd: returns nil for nil", function()
|
||
assert_is_nil(parser.parse_json_command(nil))
|
||
end)
|
||
|
||
print("\n=== Running parse_push_args tests ===\n")
|
||
|
||
local cmd = require("jj.cmd")
|
||
|
||
run_test("parse_push_args: parses bookmark and remote", function()
|
||
local opts, err = cmd.parse_push_args({ "my-bookmark", "--remote", "origin" })
|
||
assert_is_nil(err)
|
||
assert_table_equals({ bookmark = "my-bookmark", remote = "origin" }, opts)
|
||
end)
|
||
|
||
run_test("parse_push_args: parses deleted flag", function()
|
||
local opts, err = cmd.parse_push_args({ "--deleted" })
|
||
assert_is_nil(err)
|
||
assert_table_equals({ deleted = true }, opts)
|
||
end)
|
||
|
||
run_test("parse_push_args: allows remote before bookmark", function()
|
||
local opts, err = cmd.parse_push_args({ "--remote", "origin", "my-bookmark" })
|
||
assert_is_nil(err)
|
||
assert_table_equals({ bookmark = "my-bookmark", remote = "origin" }, opts)
|
||
end)
|
||
|
||
run_test("parse_push_args: errors on missing remote value", function()
|
||
local opts, err = cmd.parse_push_args({ "--remote" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Missing remote name after --remote", err)
|
||
end)
|
||
|
||
run_test("parse_push_args: errors when remote value is another option", function()
|
||
local opts, err = cmd.parse_push_args({ "--remote", "--deleted" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Missing remote name after --remote", err)
|
||
end)
|
||
|
||
run_test("parse_push_args: errors on duplicate remotes", function()
|
||
local opts, err = cmd.parse_push_args({ "--remote", "origin", "--remote", "upstream" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Remote already set. Cannot specify multiple remotes.", err)
|
||
end)
|
||
|
||
run_test("parse_push_args: errors on multiple bookmarks", function()
|
||
local opts, err = cmd.parse_push_args({ "bookmark-1", "bookmark-2" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Only one bookmark can be provided", err)
|
||
end)
|
||
|
||
run_test("parse_push_args: errors on unknown option", function()
|
||
local opts, err = cmd.parse_push_args({ "--foo" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Unknown option: --foo", err)
|
||
end)
|
||
|
||
run_test("parse_push_args: errors on deleted with bookmark", function()
|
||
local opts, err = cmd.parse_push_args({ "--deleted", "my-bookmark" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Cannot specify both --deleted and a bookmark", err)
|
||
end)
|
||
|
||
print("\n=== Running build_log_cmd tests ===\n")
|
||
|
||
local log = require("jj.cmd.log")
|
||
|
||
run_test("build_log_cmd: raw_flags with --no-pager does not duplicate it", function()
|
||
local cmd = log.build_log_cmd({ raw_flags = { "--no-pager", "--limit", "18" } })
|
||
assert_table_equals({ "jj", "log", "--no-pager", "--limit", "18" }, cmd)
|
||
end)
|
||
|
||
run_test("build_log_cmd: raw_flags without --no-pager works normally", function()
|
||
local cmd = log.build_log_cmd({ raw_flags = { "--limit", "18" } })
|
||
assert_table_equals({ "jj", "log", "--no-pager", "--limit", "18" }, cmd)
|
||
end)
|
||
|
||
run_test("build_log_cmd: raw_flags that is only --no-pager", function()
|
||
local cmd = log.build_log_cmd({ raw_flags = { "--no-pager" } })
|
||
assert_table_equals({ "jj", "log", "--no-pager" }, cmd)
|
||
end)
|
||
|
||
run_test("build_log_cmd: structured opts with limit", function()
|
||
local cmd = log.build_log_cmd({ limit = 10 })
|
||
assert_table_equals({ "jj", "log", "--no-pager", "--limit", "10" }, cmd)
|
||
end)
|
||
|
||
run_test("build_log_cmd: structured opts with revisions", function()
|
||
local cmd = log.build_log_cmd({ revisions = "main" })
|
||
assert_table_equals({ "jj", "log", "--no-pager", "--revisions", "main" }, cmd)
|
||
end)
|
||
|
||
run_test("build_log_cmd: default opts produces valid command", function()
|
||
local cmd = log.build_log_cmd({})
|
||
assert_table_equals({ "jj", "log", "--no-pager" }, cmd)
|
||
end)
|
||
|
||
print("\n=== Running resolve arg parsing tests ===\n")
|
||
|
||
local cmd = require("jj.cmd")
|
||
|
||
run_test("parse_resolve_args: defaults to @", function()
|
||
local opts, err = cmd.parse_resolve_args({})
|
||
assert_is_nil(err)
|
||
assert_table_equals({ rev = "@" }, opts)
|
||
end)
|
||
|
||
run_test("parse_resolve_args: parses revision, tool, external and filesets", function()
|
||
local opts, err = cmd.parse_resolve_args({
|
||
"-r",
|
||
"abc123",
|
||
"--tool",
|
||
"mergiraf",
|
||
"--external",
|
||
"src/",
|
||
"README.md",
|
||
})
|
||
assert_is_nil(err)
|
||
assert_table_equals({
|
||
rev = "abc123",
|
||
external = true,
|
||
args = { "--tool", "mergiraf" },
|
||
filesets = { "src/", "README.md" },
|
||
}, opts)
|
||
end)
|
||
|
||
run_test("parse_resolve_args: errors on unknown long option", function()
|
||
local opts, err = cmd.parse_resolve_args({ "--summary", "src/" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Unknown option: --summary", err)
|
||
end)
|
||
|
||
run_test("parse_resolve_args: errors on duplicate --tool", function()
|
||
local opts, err = cmd.parse_resolve_args({ "--tool", "meld", "--tool", "mergiraf" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Tool already set. Cannot specify multiple tools.", err)
|
||
end)
|
||
|
||
run_test("parse_resolve_args: errors on duplicate revision", function()
|
||
local opts, err = cmd.parse_resolve_args({ "-r", "a", "--revision", "b" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Revision already set. Cannot specify multiple revisions.", err)
|
||
end)
|
||
|
||
run_test("parse_resolve_args: errors on missing --tool value", function()
|
||
local opts, err = cmd.parse_resolve_args({ "--tool" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Missing value for --tool", err)
|
||
end)
|
||
|
||
run_test("parse_resolve_args: errors on missing -r/--revision value", function()
|
||
local opts, err = cmd.parse_resolve_args({ "-r" })
|
||
assert_is_nil(opts)
|
||
assert_equals("Missing value for --revision/-r", err)
|
||
end)
|
||
|
||
local resolve = require("jj.cmd.resolve")
|
||
|
||
run_test("resolve: passes jj-quoted argv for external execution", function()
|
||
local runner = require("jj.core.runner")
|
||
local original_execute_argv_async = runner.execute_async
|
||
local original_notify = utils.notify
|
||
local original_ensure_jj = utils.ensure_jj
|
||
|
||
local captured_cmd = nil
|
||
runner.execute_async = function(cmd)
|
||
captured_cmd = cmd
|
||
end
|
||
utils.notify = function() end
|
||
utils.ensure_jj = function()
|
||
return true
|
||
end
|
||
|
||
local ok, err = pcall(function()
|
||
resolve.resolve({
|
||
rev = "abc 123",
|
||
args = { "--tool", "my tool" },
|
||
filesets = { "dir with spaces/", "glob:*" },
|
||
external = true,
|
||
})
|
||
assert_table_equals({
|
||
"jj",
|
||
"resolve",
|
||
"--revision",
|
||
"abc 123",
|
||
"--tool",
|
||
"my tool",
|
||
jj_args.fileset("dir with spaces/"),
|
||
jj_args.fileset("glob:*"),
|
||
}, captured_cmd)
|
||
end)
|
||
|
||
runner.execute_async = original_execute_argv_async
|
||
utils.notify = original_notify
|
||
utils.ensure_jj = original_ensure_jj
|
||
if not ok then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
run_test("resolve: passes jj-quoted filesets for floating execution", function()
|
||
local terminal = require("jj.ui.terminal")
|
||
local original_run_floating = terminal.run_floating
|
||
local original_notify = utils.notify
|
||
local original_ensure_jj = utils.ensure_jj
|
||
|
||
local captured_cmd = nil
|
||
terminal.run_floating = function(cmd)
|
||
captured_cmd = cmd
|
||
end
|
||
utils.notify = function() end
|
||
utils.ensure_jj = function()
|
||
return true
|
||
end
|
||
|
||
local ok, err = pcall(function()
|
||
resolve.resolve({
|
||
rev = "abc 123",
|
||
args = { "--tool", "my tool" },
|
||
filesets = { "dir with spaces/", "glob:*" },
|
||
})
|
||
assert_table_equals({
|
||
"jj",
|
||
"resolve",
|
||
"--revision",
|
||
"abc 123",
|
||
"--tool",
|
||
"my tool",
|
||
jj_args.fileset("dir with spaces/"),
|
||
jj_args.fileset("glob:*"),
|
||
}, captured_cmd)
|
||
end)
|
||
|
||
terminal.run_floating = original_run_floating
|
||
utils.notify = original_notify
|
||
utils.ensure_jj = original_ensure_jj
|
||
if not ok then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
print("\n=== Running utils helper tests ===\n")
|
||
|
||
run_test("is_change_conflicted: returns true when jj reports conflict", function()
|
||
local runner = require("jj.core.runner")
|
||
local original_execute_argv = runner.execute
|
||
|
||
runner.execute = function(argv, error_prefix, input, silent)
|
||
assert_table_equals({ "jj", "log", "--no-graph", "-r", "abc123", "-T", "conflict", "--quiet" }, argv)
|
||
assert_equals("Error checking if revset has conflicts", error_prefix)
|
||
assert_is_nil(input)
|
||
assert_equals(true, silent)
|
||
return "true\n", true
|
||
end
|
||
|
||
local ok, err = pcall(function()
|
||
assert_equals(true, utils.is_change_conflicted("abc123"))
|
||
end)
|
||
runner.execute = original_execute_argv
|
||
if not ok then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
run_test("is_change_conflicted: returns false when jj reports no conflict", function()
|
||
local runner = require("jj.core.runner")
|
||
local original_execute_argv = runner.execute
|
||
|
||
runner.execute = function()
|
||
return "false\n", true
|
||
end
|
||
|
||
local ok, err = pcall(function()
|
||
assert_equals(false, utils.is_change_conflicted("abc123"))
|
||
end)
|
||
runner.execute = original_execute_argv
|
||
if not ok then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
run_test("is_change_conflicted: returns false when jj command fails", function()
|
||
local runner = require("jj.core.runner")
|
||
local original_execute_argv = runner.execute
|
||
|
||
runner.execute = function()
|
||
return nil, false
|
||
end
|
||
|
||
local ok, err = pcall(function()
|
||
assert_equals(false, utils.is_change_conflicted("abc123"))
|
||
end)
|
||
runner.execute = original_execute_argv
|
||
if not ok then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
print("\n=== Running utils.parse_bookmark_names tests ===\n")
|
||
|
||
run_test("parse_bookmark_names: parses simple bookmark", function()
|
||
local input = "main::true"
|
||
assert_table_equals({ { name = "main", is_deleted = false } }, utils.parse_bookmark_names(input))
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: parses multiple bookmarks", function()
|
||
local input = "main::true feature-1::true feature-2::false"
|
||
assert_table_equals({
|
||
{ name = "main", is_deleted = false },
|
||
{ name = "feature-1", is_deleted = false },
|
||
{ name = "feature-2", is_deleted = true },
|
||
}, utils.parse_bookmark_names(input))
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: strips asterisks", function()
|
||
local input = "main*::true feature-1*::true"
|
||
assert_table_equals(
|
||
{ { name = "main", is_deleted = false }, { name = "feature-1", is_deleted = false } },
|
||
utils.parse_bookmark_names(input)
|
||
)
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: strips remote suffixes", function()
|
||
local input = "main@origin::true feature-1@remote::false"
|
||
assert_table_equals(
|
||
{ { name = "main", is_deleted = false }, { name = "feature-1", is_deleted = true } },
|
||
utils.parse_bookmark_names(input)
|
||
)
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: deduplicates bookmarks", function()
|
||
local input = "main::true main@origin::true main*::true"
|
||
assert_table_equals({ { name = "main", is_deleted = false } }, utils.parse_bookmark_names(input))
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: handles mixed input", function()
|
||
local input = "main*::true feature-1::true feature-1@origin::true feature-2*::false"
|
||
assert_table_equals({
|
||
{ name = "main", is_deleted = false },
|
||
{ name = "feature-1", is_deleted = false },
|
||
{ name = "feature-2", is_deleted = true },
|
||
}, utils.parse_bookmark_names(input))
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: handles empty input", function()
|
||
assert_table_equals({}, utils.parse_bookmark_names(""))
|
||
end)
|
||
|
||
run_test("parse_bookmark_names: handles whitespace only", function()
|
||
assert_table_equals({}, utils.parse_bookmark_names(" "))
|
||
end)
|
||
|
||
print("\n=== Running file encoding tests ===\n")
|
||
|
||
local jj_file = require("jj.file")
|
||
|
||
--- Hex-dump a string so assertion failures on binary data are readable.
|
||
local function hex(s)
|
||
return (s:gsub(".", function(c)
|
||
return string.format("%02x ", string.byte(c))
|
||
end))
|
||
end
|
||
|
||
run_test("decode: utf-16le with BOM and dos endings", function()
|
||
-- "a\r\nü\r\n" in utf-16le with BOM
|
||
local raw = "\255\254a\0\r\0\n\0\252\0\r\0\n\0"
|
||
local lines, had_eol, enc = jj_file._decode(raw)
|
||
assert_table_equals({ "a", "\195\188" }, lines) -- "ü" in utf-8
|
||
assert_equals(true, had_eol)
|
||
assert_equals("utf-16le", enc.fenc)
|
||
assert_equals(true, enc.bomb)
|
||
assert_equals("dos", enc.ff)
|
||
for _, line in ipairs(lines) do
|
||
assert_is_nil(line:find("%z"), "line contains a NUL byte")
|
||
assert_is_nil(line:find("\1"), "line contains a SOH byte")
|
||
end
|
||
end)
|
||
|
||
run_test("decode: honors explicit enc for BOM-less utf-16le", function()
|
||
local lines = jj_file._decode("h\0i\0", { fenc = "utf-16le", bomb = false, ff = "unix" })
|
||
assert_table_equals({ "hi" }, lines)
|
||
end)
|
||
|
||
run_test("decode: roundtrip reproduces the bytes exactly", function()
|
||
local fixtures = {
|
||
["utf-16le bom"] = "\255\254h\0i\0\n\0\252\0",
|
||
["utf-16be bom"] = "\254\255\0h\0i\0\n\0\252",
|
||
["utf-16le bom + dos"] = "\255\254a\0\r\0\n\0b\0\r\0\n\0",
|
||
["utf-8 bom"] = "\239\187\191hello\nworld\n",
|
||
["plain dos"] = "a\r\nb\r\n",
|
||
["mac"] = "a\rb\r",
|
||
["no trailing newline"] = "a\nb",
|
||
["empty file"] = "",
|
||
}
|
||
for name, raw in pairs(fixtures) do
|
||
local lines, had_eol, enc = jj_file._decode(raw)
|
||
if lines == nil then
|
||
error(string.format("%s: decode failed: %s", name, tostring(had_eol)))
|
||
end
|
||
local encoded, err = jj_file._encode(lines, had_eol, enc)
|
||
if encoded == nil then
|
||
error(string.format("%s: encode failed: %s", name, tostring(err)))
|
||
end
|
||
assert_equals(hex(raw), hex(encoded), name)
|
||
end
|
||
end)
|
||
|
||
print("\n=== Running get_file_content tests ===\n")
|
||
|
||
run_test("get_file_content: reads existing file content", function()
|
||
local runner = require("jj.core.runner")
|
||
local original = runner.execute_raw
|
||
runner.execute_raw = function(cmd)
|
||
assert_table_equals({ "jj", "file", "show", "-r", "abc123", jj_args.fileset("src/file.py") }, cmd)
|
||
return "a\nb\n", true, ""
|
||
end
|
||
local ok_test, err = pcall(function()
|
||
local lines, had_eol, ok, _, absent = jj_file.get_file_content("abc123", "src/file.py")
|
||
assert_table_equals({ "a", "b" }, lines)
|
||
assert_equals(true, had_eol)
|
||
assert_equals(true, ok)
|
||
assert_equals(false, absent)
|
||
end)
|
||
runner.execute_raw = original
|
||
if not ok_test then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
run_test("get_file_content: absent path in revision reports absent (not a read error)", function()
|
||
local runner = require("jj.core.runner")
|
||
local original = runner.execute_raw
|
||
runner.execute_raw = function(cmd)
|
||
assert_table_equals({ "jj", "file", "show", "-r", "abc123", jj_args.fileset("src/new_file.py") }, cmd)
|
||
return nil, false, "Error: No such path: src/new_file.py\n"
|
||
end
|
||
local ok_test, err = pcall(function()
|
||
local lines, had_eol, ok, _, absent = jj_file.get_file_content("abc123", "src/new_file.py")
|
||
assert_table_equals({}, lines)
|
||
assert_equals(false, had_eol)
|
||
assert_equals(false, ok)
|
||
assert_equals(true, absent)
|
||
end)
|
||
runner.execute_raw = original
|
||
if not ok_test then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
run_test("get_file_content: genuine read error returns failure without absent", function()
|
||
local runner = require("jj.core.runner")
|
||
local original = runner.execute_raw
|
||
runner.execute_raw = function(cmd)
|
||
assert_table_equals({ "jj", "file", "show", "-r", "nope", jj_args.fileset("src/file.py") }, cmd)
|
||
return nil, false, "Error: Revision `nope` doesn't exist\n"
|
||
end
|
||
local ok_test, err = pcall(function()
|
||
local _, _, ok, _, absent = jj_file.get_file_content("nope", "src/file.py")
|
||
assert_equals(false, ok)
|
||
assert_equals(false, absent)
|
||
end)
|
||
runner.execute_raw = original
|
||
if not ok_test then
|
||
error(err)
|
||
end
|
||
end)
|
||
|
||
-- Print summary
|
||
print(string.format("\n=== Test Summary ==="))
|
||
print(string.format("Passed: %d", tests_passed))
|
||
print(string.format("Failed: %d", tests_failed))
|
||
|
||
if tests_failed > 0 then
|
||
print("\n=== Failures ===")
|
||
for _, failure in ipairs(failures) do
|
||
print(string.format("\n%s:", failure.name))
|
||
print(string.format(" %s", failure.error))
|
||
end
|
||
os.exit(1)
|
||
else
|
||
print("\n✓ All tests passed!")
|
||
os.exit(0)
|
||
end
|