From 39de3c6bc9c68144a1ef1dd8c9966a193adfaa3a Mon Sep 17 00:00:00 2001 From: Nicolas GB Date: Wed, 26 Nov 2025 09:42:45 +0100 Subject: [PATCH] fix: Improve the parser and add test to handle edge cases (#42) --- lua/jj/core/parser.lua | 51 ++++---- tests/run_tests.lua | 265 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+), 25 deletions(-) create mode 100755 tests/run_tests.lua diff --git a/lua/jj/core/parser.lua b/lua/jj/core/parser.lua index dc11b24..a38d3be 100644 --- a/lua/jj/core/parser.lua +++ b/lua/jj/core/parser.lua @@ -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 diff --git a/tests/run_tests.lua b/tests/run_tests.lua new file mode 100755 index 0000000..cefc5d5 --- /dev/null +++ b/tests/run_tests.lua @@ -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