fix: Improve the parser and add test to handle edge cases (#42)

This commit is contained in:
Nicolas GB
2025-11-26 09:42:45 +01:00
committed by GitHub
parent a9f0ca0031
commit 39de3c6bc9
2 changed files with 291 additions and 25 deletions
+26 -25
View File
@@ -113,37 +113,38 @@ end
--- @param line string The log line to parse
--- @return string|nil The revision ID if found, nil otherwise
function M.get_rev_from_log_line(line)
-- Define jujutsu symbols with their UTF-8 byte sequences
local jj_symbols = {
diamond = "\226\151\134", -- ◆ U+25C6
circle = "\226\151\139", -- ○ U+25CB
conflict = "\195\151", -- × U+00D7
-- Build pattern to match graph characters and symbols at start of line
-- Include: box-drawing chars, whitespace, jujutsu UTF-8 symbols, and ASCII markers
local graph_chars = "│┃┆┇┊┋╭╮╰╯├┤┬┴┼─└┘┌┐%s" -- box-drawing + whitespace
-- Jujutsu UTF-8 symbols (with their byte sequences)
local utf8_symbols = {
"\226\151\134", -- ◆ U+25C6 (diamond)
"\226\151\139", -- ○ U+25CB (circle)
"\195\151", -- × U+00D7 (conflict)
}
local revset
-- ASCII markers (escaped for pattern matching)
local ascii_markers = { "@", "%*", "/", "\\", "%-", "%+", "|" }
-- Try each symbol pattern
for _, symbol in pairs(jj_symbols) do
-- Pattern: Lines starting with symbol
revset = line:match("^%s*" .. symbol .. "%s+(%w+)")
if revset then
return revset
end
-- Pattern: Lines with │ followed by symbol (this are the branches)
revset = line:match("^│%s*" .. symbol .. "%s+(%w+)")
if revset then
return revset
end
-- Build character class for allowed prefix
local allowed_prefix = "[" .. graph_chars
for _, symbol in ipairs(utf8_symbols) do
allowed_prefix = allowed_prefix .. symbol
end
-- Pattern for simple ASCII symbols
revset = line:match("^%s*[@]%s+(%w+)")
if revset then
return revset
for _, marker in ipairs(ascii_markers) do
allowed_prefix = allowed_prefix .. marker
end
allowed_prefix = allowed_prefix .. "]+" -- close class, match one or more (not zero)
return nil
-- Match first alphanumeric sequence after graph prefix
-- Only match if it's followed by whitespace or end of string (not part of text)
local revset = line:match("^" .. allowed_prefix .. "(%w+)%s")
if not revset then
-- Try matching at end of line without trailing whitespace
revset = line:match("^" .. allowed_prefix .. "(%w+)$")
end
return revset
end
return M
+265
View File
@@ -0,0 +1,265 @@
#!/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 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 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_rev_from_log_line(line))
end)
run_test("parses simple circle symbol", function()
local line = "○ def456 another commit"
assert_equals("def456", parser.get_rev_from_log_line(line))
end)
run_test("parses simple @ symbol", function()
local line = "@ def456 another commit"
assert_equals("def456", parser.get_rev_from_log_line(line))
end)
run_test("parses conflict symbol", function()
local line = "× ghi789 conflicted commit"
assert_equals("ghi789", parser.get_rev_from_log_line(line))
end)
run_test("parses with leading whitespace", function()
local line = " ◆ jkl012 indented commit"
assert_equals("jkl012", parser.get_rev_from_log_line(line))
end)
run_test("parses single branch with box drawing", function()
local line = "│ ○ mno345 commit on branch"
assert_equals("mno345", parser.get_rev_from_log_line(line))
end)
run_test("parses multiple branches", function()
local line = "│ │ ◆ pqr678 commit with multiple branches"
assert_equals("pqr678", parser.get_rev_from_log_line(line))
end)
run_test("parses complex graph with connectors", function()
local line = "├─○ stu901 commit after merge"
assert_equals("stu901", parser.get_rev_from_log_line(line))
end)
run_test("parses graph with multiple box chars", function()
local line = "│ ├─◆ vwx234 complex branch"
assert_equals("vwx234", parser.get_rev_from_log_line(line))
end)
run_test("parses ASCII @ symbol", function()
local line = "@ yza567 current working copy"
assert_equals("yza567", parser.get_rev_from_log_line(line))
end)
run_test("parses ASCII * symbol (git-style)", function()
local line = "* bcd890 git style commit"
assert_equals("bcd890", parser.get_rev_from_log_line(line))
end)
run_test("parses ASCII graph with pipe", function()
local line = "| * efg123 ascii branch"
assert_equals("efg123", parser.get_rev_from_log_line(line))
end)
run_test("parses mixed ASCII graph", function()
local line = "|\\ @ hij456 merge commit"
assert_equals("hij456", parser.get_rev_from_log_line(line))
end)
run_test("parses @ not at top", function()
local line = "│ @ klm789 current change in middle"
assert_equals("klm789", parser.get_rev_from_log_line(line))
end)
run_test("parses @ with multiple branches", function()
local line = "│ │ @ nop012 working copy on branch"
assert_equals("nop012", parser.get_rev_from_log_line(line))
end)
run_test("parses @ after merge connector", function()
local line = "├─@ qrs345 working copy after merge"
assert_equals("qrs345", parser.get_rev_from_log_line(line))
end)
run_test("parses with various box drawing characters", function()
local line = "╭─╮ ○ tuv678 fancy box"
assert_equals("tuv678", parser.get_rev_from_log_line(line))
end)
run_test("parses deeply nested branches", function()
local line = "│ │ │ │ ◆ wxy901 deeply nested"
assert_equals("wxy901", parser.get_rev_from_log_line(line))
end)
run_test("parses revision with numbers and letters", function()
local line = "◆ abc123def456 mixed alphanumeric"
assert_equals("abc123def456", parser.get_rev_from_log_line(line))
end)
run_test("stops at first non-alphanumeric after revision", function()
local line = "○ xyz789 this is the message"
assert_equals("xyz789", parser.get_rev_from_log_line(line))
end)
run_test("parses with different line connector styles", function()
local line = "┼─┤ ◆ zab234 cross connector"
assert_equals("zab234", parser.get_rev_from_log_line(line))
end)
run_test("parses with curve connectors", function()
local line = "╰─○ cde567 curve connector"
assert_equals("cde567", parser.get_rev_from_log_line(line))
end)
run_test("parses with double line vertical ┃", function()
local line = "┃ ◆ fgh890 double line vertical"
assert_equals("fgh890", parser.get_rev_from_log_line(line))
end)
run_test("parses with light triple dash vertical ┆", function()
local line = "┆ ○ ijk123 light triple dash"
assert_equals("ijk123", parser.get_rev_from_log_line(line))
end)
run_test("parses with heavy triple dash vertical ┇", function()
local line = "┇ ◆ lmn456 heavy triple dash"
assert_equals("lmn456", parser.get_rev_from_log_line(line))
end)
run_test("parses with light quadruple dash vertical ┊", function()
local line = "┊ ○ opq789 light quadruple dash"
assert_equals("opq789", parser.get_rev_from_log_line(line))
end)
run_test("parses with heavy quadruple dash vertical ┋", function()
local line = "┋ ◆ rst012 heavy quadruple dash"
assert_equals("rst012", parser.get_rev_from_log_line(line))
end)
run_test("parses with top-left corner ┌", function()
local line = "┌─○ uvw345 top left corner"
assert_equals("uvw345", parser.get_rev_from_log_line(line))
end)
run_test("parses with top-right corner ┐", function()
local line = "┐ ◆ xyz678 top right corner"
assert_equals("xyz678", parser.get_rev_from_log_line(line))
end)
run_test("parses with bottom-left corner └", function()
local line = "└─○ abc901 bottom left corner"
assert_equals("abc901", parser.get_rev_from_log_line(line))
end)
run_test("parses with bottom-right corner ┘", function()
local line = "┘ ◆ def234 bottom right corner"
assert_equals("def234", parser.get_rev_from_log_line(line))
end)
run_test("parses with left tee ├", function()
local line = "├ ○ ghi567 left tee"
assert_equals("ghi567", parser.get_rev_from_log_line(line))
end)
run_test("parses with right tee ┤", function()
local line = "┤ ◆ jkl890 right tee"
assert_equals("jkl890", parser.get_rev_from_log_line(line))
end)
run_test("parses with top tee ┬", function()
local line = "┬─○ mno123 top tee"
assert_equals("mno123", parser.get_rev_from_log_line(line))
end)
run_test("parses with bottom tee ┴", function()
local line = "┴─◆ pqr456 bottom tee"
assert_equals("pqr456", parser.get_rev_from_log_line(line))
end)
run_test("parses with cross ┼", function()
local line = "┼ ○ stu789 cross"
assert_equals("stu789", parser.get_rev_from_log_line(line))
end)
run_test("parses with mixed special box chars", function()
local line = "┃ ┆ ┇ ○ vwx012 mixed special"
assert_equals("vwx012", parser.get_rev_from_log_line(line))
end)
run_test("parses with rounded corners", function()
local line = "╭─╮ ╰─╯ ◆ yza345 rounded corners"
assert_equals("yza345", parser.get_rev_from_log_line(line))
end)
run_test("returns nil for lines without revision", function()
local line = "This is just a description line"
assert_is_nil(parser.get_rev_from_log_line(line))
end)
run_test("returns nil for empty line", function()
local line = ""
assert_is_nil(parser.get_rev_from_log_line(line))
end)
run_test("returns nil for only graph characters", function()
local line = "│ │ ├─"
assert_is_nil(parser.get_rev_from_log_line(line))
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