Skip to content

Commit 69c5c84

Browse files
committed
Add unified diff support for luatest.assert_equals
This patch adds unified diff output for `t.assert_equals()` failures when the test suite is run with the `--diff` option, using a vendored Lua implementation of google/diff-match-patch (`luatest/vendor/diff_match_patch.lua` taken from [^1]). Closes #412 [^1]: https://github.com/google/diff-match-patch/blob/master/lua/diff_match_patch.lua
1 parent 731ba6f commit 69c5c84

File tree

8 files changed

+2346
-1
lines changed

8 files changed

+2346
-1
lines changed

.luacheckrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include_files = {"**/*.lua", "*.rockspec", "*.luacheckrc"}
2-
exclude_files = {"build.luarocks/", "lua_modules/", "tmp/", ".luarocks/", ".rocks/"}
2+
exclude_files = {"build.luarocks/", "lua_modules/", "tmp/", ".luarocks/", ".rocks/", "luatest/vendor/"}
33

44
max_line_length = 120

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Added support for unified diff output in `t.assert_equals()` failure messages.
6+
The diff is shown only when the test suite is run with the `--diff` option (gh-412).
7+
38
## 1.3.0
49

510
- Fixed a bug when `assert_covers` treats arrays as maps (gh-405).

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ You can configure luatest or run any bootstrap code there.
123123
See the `getting-started example <https://github.com/tarantool/cartridge-cli/tree/master/examples/getting-started-app/test>`_
124124
in cartridge-cli repo.
125125

126+
Enable unified diffs for equality assertions by starting ``luatest`` with ``--diff``.
127+
When enabled, failing comparisons of strings, tables, or msgpack-encoded tables will
128+
include a unified diff alongside the existing mismatch analysis to make debugging easier.
129+
126130
---------------------------------
127131
Tests order
128132
---------------------------------

luatest/assertions.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
local math = require('math')
77

88
local comparator = require('luatest.comparator')
9+
local diff = require('luatest.diff')
910
local mismatch_formatter = require('luatest.mismatch_formatter')
1011
local pp = require('luatest.pp')
1112
local log = require('luatest.log')
@@ -20,12 +21,17 @@ local prettystr_pairs = pp.tostring_pair
2021
local M = {}
2122

2223
local xfail = false
24+
local diff_enabled = false
2325

2426
local box_error_type = ffi.typeof(box.error.new(box.error.UNKNOWN))
2527

2628
-- private exported functions (for testing)
2729
M.private = {}
2830

31+
function M.private.set_diff_enabled(value)
32+
diff_enabled = value and true or false
33+
end
34+
2935
function M.private.is_xfail()
3036
local xfail_status = xfail
3137
xfail = false
@@ -83,6 +89,14 @@ local function error_msg_equality(actual, expected, deep_analysis)
8389
if success then
8490
result = table.concat({result, mismatchResult}, '\n')
8591
end
92+
93+
if diff_enabled then
94+
local diff_result = diff.unified(expected, actual)
95+
if diff_result then
96+
result = table.concat({result, 'diff:', diff_result}, '\n')
97+
end
98+
end
99+
86100
return result
87101
end
88102
return string.format("expected: %s, actual: %s",

luatest/diff.lua

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
local yaml = require('yaml')
2+
local msgpack = require('msgpack')
3+
4+
-- diff_match_patch expects bit32
5+
if not rawget(_G, 'bit32') then
6+
_G.bit32 = require('bit')
7+
end
8+
9+
local diff_match_patch = require('luatest.vendor.diff_match_patch')
10+
11+
diff_match_patch.settings({
12+
Diff_Timeout = 0,
13+
Patch_Margin = 1e9,
14+
})
15+
16+
local M = {}
17+
18+
local function encode_yaml(value)
19+
local ok, encoded = pcall(yaml.encode, value)
20+
if ok then
21+
return encoded
22+
end
23+
end
24+
25+
local function msgpack_to_yaml(value)
26+
if type(value) ~= 'string' then
27+
return nil
28+
end
29+
30+
local ok, decoded = pcall(msgpack.decode, value)
31+
if not ok or type(decoded) ~= 'table' then
32+
return nil
33+
end
34+
35+
return encode_yaml(decoded)
36+
end
37+
38+
local function as_text(value)
39+
if type(value) == 'table' then
40+
return encode_yaml(value)
41+
end
42+
43+
if type(value) == 'string' then
44+
return msgpack_to_yaml(value) or value
45+
end
46+
end
47+
48+
local function url_unescape(s)
49+
return s:gsub("%%(%x%x)", function(h)
50+
return string.char(tonumber(h, 16))
51+
end)
52+
end
53+
54+
local function prettify_patch(patch_text)
55+
patch_text = url_unescape(patch_text)
56+
57+
local out = {}
58+
59+
for line in (patch_text .. '\n'):gmatch('(.-)\n') do
60+
if line ~= '' and line ~= ' ' then
61+
local first = line:sub(1, 1)
62+
63+
if first ~= '@' and first ~= '+'
64+
and first ~= '-' and first ~= ' ' then
65+
line = ' ' .. line
66+
end
67+
68+
table.insert(out, line)
69+
end
70+
end
71+
72+
return table.concat(out, '\n')
73+
end
74+
75+
function M.unified(expected, actual)
76+
local expected_text = as_text(expected)
77+
local actual_text = as_text(actual)
78+
79+
if expected_text == nil or actual_text == nil then
80+
return nil
81+
end
82+
83+
local diffs = diff_match_patch.diff_main(expected_text, actual_text)
84+
diff_match_patch.diff_cleanupSemantic(diffs)
85+
86+
local patches = diff_match_patch.patch_make(expected_text,
87+
actual_text, diffs)
88+
local patch_text = diff_match_patch.patch_toText(patches)
89+
90+
if patch_text == '' then
91+
return nil
92+
end
93+
94+
return prettify_patch(patch_text)
95+
end
96+
97+
return M

luatest/runner.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ function Runner.run(args, options)
5757
end
5858
options = utils.merge(options.luatest.configure(), Runner.parse_cmd_line(args), options)
5959

60+
assertions.private.set_diff_enabled(options.diff)
61+
6062
log.initialize({
6163
vardir = Server.vardir,
6264
log_file = options.log_file,
@@ -118,6 +120,7 @@ Options:
118120
May be repeated to exclude several patterns
119121
Make sure you escape magic chars like +? with %
120122
--coverage: Use luacov to collect code coverage.
123+
--diff: Enable diff output for equality assertion failures.
121124
--no-clean: Disable the var directory (default: /tmp/t) deletion before
122125
running tests.
123126
--list-test-cases List all test cases.
@@ -190,6 +193,8 @@ function Runner.parse_cmd_line(args)
190193
result.enable_capture = false
191194
elseif arg == '--coverage' then
192195
result.coverage_report = true
196+
elseif arg == '--diff' then
197+
result.diff = true
193198
elseif arg == '--no-clean' then
194199
result.no_clean = true
195200
elseif arg == '--list-test-cases' then

0 commit comments

Comments
 (0)