Skip to content

Commit d470e14

Browse files
committed
Add line diff support
This patch adds a simple line-by-line diff output for `t.assert_equals()` and `t.assert_covers()` failures. Closes #412
1 parent a0930d4 commit d470e14

File tree

4 files changed

+415
-12
lines changed

4 files changed

+415
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Added a simple line-by-line diff to `t.assert_equals()` and `t.assert_covers()`
6+
failure messages (gh-412).
57
- Fixed a bug when the JUnit reporter generated invalid XML for parameterized
68
tests with string arguments (gh-407).
79
- Group and suite hooks must now be registered using the call-style

luatest/assertions.lua

Lines changed: 20 additions & 1 deletion
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')
@@ -83,6 +84,12 @@ local function error_msg_equality(actual, expected, deep_analysis)
8384
if success then
8485
result = table.concat({result, mismatchResult}, '\n')
8586
end
87+
88+
local diff_result = diff.build_line_diff(expected, actual)
89+
if diff_result then
90+
result = table.concat({result, 'diff:', diff_result}, '\n')
91+
end
92+
8693
return result
8794
end
8895
return string.format("expected: %s, actual: %s",
@@ -470,7 +477,19 @@ end
470477
function M.assert_covers(actual, expected, message)
471478
if not table_covers(actual, expected) then
472479
local str_actual, str_expected = prettystr_pairs(actual, expected)
473-
failure(string.format('expected %s to cover %s', str_actual, str_expected), message, 2)
480+
local sliced_actual = table_slice(actual, expected)
481+
482+
local parts = {
483+
string.format('expected %s to cover %s', str_actual, str_expected),
484+
}
485+
486+
local diff_result = diff.build_line_diff(expected, sliced_actual)
487+
if diff_result then
488+
table.insert(parts, 'diff:')
489+
table.insert(parts, diff_result)
490+
end
491+
492+
failure(table.concat(parts, '\n'), message, 2)
474493
end
475494
end
476495

luatest/diff.lua

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
local pp = require("luatest.pp")
2+
3+
local M = {}
4+
5+
local function diff_by_lines(text1, text2)
6+
local lines1 = string.split(text1, '\n')
7+
local lines2 = string.split(text2, '\n')
8+
9+
local m = #lines1
10+
local n = #lines2
11+
local lcs = {}
12+
13+
for i = 0, m do
14+
lcs[i] = {}
15+
lcs[i][0] = 0
16+
end
17+
18+
for j = 0, n do
19+
lcs[0][j] = 0
20+
end
21+
22+
for i = 1, m do
23+
for j = 1, n do
24+
if lines1[i] == lines2[j] then
25+
lcs[i][j] = lcs[i - 1][j - 1] + 1
26+
else
27+
local left = lcs[i - 1][j]
28+
local top = lcs[i][j - 1]
29+
lcs[i][j] = left >= top and left or top
30+
end
31+
end
32+
end
33+
34+
local diffs = {}
35+
local i = m
36+
local j = n
37+
38+
while i > 0 or j > 0 do
39+
if i > 0 and j > 0 and lines1[i] == lines2[j] then
40+
table.insert(diffs, 1, {'equal', lines1[i]})
41+
i = i - 1
42+
j = j - 1
43+
elseif j > 0 and (i == 0 or lcs[i][j - 1] >= lcs[i - 1][j]) then
44+
table.insert(diffs, 1, {'insert', lines2[j]})
45+
j = j - 1
46+
else
47+
table.insert(diffs, 1, {'delete', lines1[i]})
48+
i = i - 1
49+
end
50+
end
51+
52+
return diffs
53+
end
54+
55+
local function prettify_patch(diffs)
56+
local out = {}
57+
58+
for _, diff in ipairs(diffs) do
59+
local tag, line = diff[1], diff[2]
60+
if tag == 'equal' then
61+
table.insert(out, ' ' .. line)
62+
elseif tag == 'delete' then
63+
table.insert(out, '-' .. line)
64+
elseif tag == 'insert' then
65+
table.insert(out, '+' .. line)
66+
end
67+
end
68+
69+
if #out == 0 then
70+
return nil
71+
end
72+
73+
return table.concat(out, '\n')
74+
end
75+
76+
--- Build a simple line-by-line diff for expected and actual values
77+
-- serialized to text. Returns nil when values can't be serialized
78+
-- or there is no diff.
79+
function M.build_line_diff(expected, actual)
80+
local old = pp.LINE_LENGTH
81+
pp.LINE_LENGTH = 0
82+
local expected_text = pp.tostring(expected)
83+
local actual_text = pp.tostring(actual)
84+
pp.LINE_LENGTH = old
85+
86+
if expected_text == nil or actual_text == nil then
87+
return nil
88+
end
89+
90+
if expected_text == actual_text then
91+
return nil
92+
end
93+
94+
local diffs = diff_by_lines(expected_text, actual_text)
95+
local patch_text = prettify_patch(diffs)
96+
97+
if patch_text == '' or patch_text == nil then
98+
return nil
99+
end
100+
101+
return patch_text
102+
end
103+
104+
return M

0 commit comments

Comments
 (0)