test-unit-ext 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/NEWS.en +6 -0
- data/NEWS.ja +6 -0
- data/html/news.html +9 -1
- data/html/news.html.en +9 -1
- data/html/news.html.ja +9 -1
- data/lib/test-unit-ext.rb +2 -0
- data/lib/test-unit-ext/assertions.rb +30 -0
- data/lib/test-unit-ext/diff.rb +375 -83
- data/lib/test-unit-ext/priority.rb +2 -2
- data/lib/test-unit-ext/version.rb +1 -1
- data/lib/test-unit-ext/xml-report.rb +226 -0
- data/test/test_diff.rb +275 -34
- data/test/test_metadata.rb +14 -5
- data/test/test_xml_report.rb +163 -0
- metadata +6 -2
data/NEWS.en
CHANGED
data/NEWS.ja
CHANGED
data/html/news.html
CHANGED
@@ -9,7 +9,15 @@
|
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<h1><a name="label-0" id="label-0">NEWS</a></h1><!-- RDLabel: "NEWS" -->
|
12
|
-
<h2><a name="label-1" id="label-1">0.
|
12
|
+
<h2><a name="label-1" id="label-1">0.2.0: 2008-03-03</a></h2><!-- RDLabel: "0.2.0: 2008-03-03" -->
|
13
|
+
<ul>
|
14
|
+
<li>Supported XML report.
|
15
|
+
<ul>
|
16
|
+
<li>Added --xml-report option.</li>
|
17
|
+
</ul></li>
|
18
|
+
<li>Supported diff output for assert_equal.</li>
|
19
|
+
</ul>
|
20
|
+
<h2><a name="label-2" id="label-2">0.1.0: 2008-02-21</a></h2><!-- RDLabel: "0.1.0: 2008-02-21" -->
|
13
21
|
<ul>
|
14
22
|
<li>Initial release.</li>
|
15
23
|
</ul>
|
data/html/news.html.en
CHANGED
@@ -9,7 +9,15 @@
|
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<h1><a name="label-0" id="label-0">NEWS</a></h1><!-- RDLabel: "NEWS" -->
|
12
|
-
<h2><a name="label-1" id="label-1">0.
|
12
|
+
<h2><a name="label-1" id="label-1">0.2.0: 2008-03-03</a></h2><!-- RDLabel: "0.2.0: 2008-03-03" -->
|
13
|
+
<ul>
|
14
|
+
<li>Supported XML report.
|
15
|
+
<ul>
|
16
|
+
<li>Added --xml-report option.</li>
|
17
|
+
</ul></li>
|
18
|
+
<li>Supported diff output for assert_equal.</li>
|
19
|
+
</ul>
|
20
|
+
<h2><a name="label-2" id="label-2">0.1.0: 2008-02-21</a></h2><!-- RDLabel: "0.1.0: 2008-02-21" -->
|
13
21
|
<ul>
|
14
22
|
<li>Initial release.</li>
|
15
23
|
</ul>
|
data/html/news.html.ja
CHANGED
@@ -9,7 +9,15 @@
|
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<h1><a name="label-0" id="label-0">NEWS.ja</a></h1><!-- RDLabel: "NEWS.ja" -->
|
12
|
-
<h2><a name="label-1" id="label-1">0.
|
12
|
+
<h2><a name="label-1" id="label-1">0.2.0: 2008-03-03</a></h2><!-- RDLabel: "0.2.0: 2008-03-03" -->
|
13
|
+
<ul>
|
14
|
+
<li>XML出力のサポート。
|
15
|
+
<ul>
|
16
|
+
<li>--xml-reportオプションの追加。</li>
|
17
|
+
</ul></li>
|
18
|
+
<li>assert_equal時のdiff出力のサポート。</li>
|
19
|
+
</ul>
|
20
|
+
<h2><a name="label-2" id="label-2">0.1.0: 2008-02-21</a></h2><!-- RDLabel: "0.1.0: 2008-02-21" -->
|
13
21
|
<ul>
|
14
22
|
<li>最初のリリース。</li>
|
15
23
|
</ul>
|
data/lib/test-unit-ext.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "test/unit/testcase"
|
2
|
+
|
3
|
+
module Test
|
4
|
+
module Unit
|
5
|
+
module AssertionsWithDiff
|
6
|
+
def assert_equal(expected, actual, message=nil)
|
7
|
+
super
|
8
|
+
rescue AssertionFailedError
|
9
|
+
expected = PP.pp(expected, "") unless expected.is_a?(String)
|
10
|
+
actual = PP.pp(actual, "") unless actual.is_a?(String)
|
11
|
+
diff = Diff.readable(expected, actual)
|
12
|
+
if /^[\?\-\+].{79}/ =~ diff
|
13
|
+
folded_diff = Diff.readable(fold(expected), fold(actual))
|
14
|
+
diff = "#{diff}\nfolded diff:\n#{folded_diff}"
|
15
|
+
end
|
16
|
+
raise $!, "#{$!.message}\ndiff:\n#{diff}", $@
|
17
|
+
end
|
18
|
+
|
19
|
+
def fold(string)
|
20
|
+
string.split(/\n/).collect do |line|
|
21
|
+
line.gsub(/(.{78})/, "\\1\n")
|
22
|
+
end.join("\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class TestCase
|
27
|
+
include AssertionsWithDiff
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/test-unit-ext/diff.rb
CHANGED
@@ -1,102 +1,59 @@
|
|
1
|
-
# port of
|
1
|
+
# port of Python's difflib.
|
2
2
|
|
3
3
|
module Test
|
4
4
|
module Diff
|
5
5
|
class SequenceMatcher
|
6
|
-
def initialize(from, to)
|
6
|
+
def initialize(from, to, &junk_predicate)
|
7
7
|
@from = from
|
8
8
|
@to = to
|
9
|
+
@junk_predicate = junk_predicate
|
9
10
|
update_to_indexes
|
10
11
|
end
|
11
12
|
|
12
13
|
def longest_match(from_start, from_end, to_start, to_end)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
if k > best_size
|
22
|
-
best_from, best_to, best_size = i - k + 1, j - k + 1, k
|
23
|
-
end
|
24
|
-
end
|
25
|
-
lengths = new_lengths
|
26
|
-
end
|
27
|
-
|
28
|
-
while best_from > from_start and best_to > to_start and
|
29
|
-
@from[best_from - 1] == @to[best_to - 1]
|
30
|
-
best_from -= 1
|
31
|
-
best_to -= 1
|
32
|
-
best_size += 1
|
33
|
-
end
|
34
|
-
|
35
|
-
while best_from + best_size <= from_end and
|
36
|
-
best_to + best_size <= to_end and
|
37
|
-
@from[best_from + best_size] == @to[best_to + best_size]
|
38
|
-
best_size += 1
|
14
|
+
best_info = find_best_match_position(from_start, from_end,
|
15
|
+
to_start, to_end)
|
16
|
+
unless @junks.empty?
|
17
|
+
args = [from_start, from_end, to_start, to_end]
|
18
|
+
best_info = adjust_best_info_with_junk_predicate(false, best_info,
|
19
|
+
*args)
|
20
|
+
best_info = adjust_best_info_with_junk_predicate(true, best_info,
|
21
|
+
*args)
|
39
22
|
end
|
40
23
|
|
41
|
-
|
24
|
+
best_info
|
42
25
|
end
|
43
26
|
|
44
|
-
def
|
45
|
-
|
46
|
-
blocks = []
|
47
|
-
until queue.empty?
|
48
|
-
from_start, from_end, to_start, to_end = queue.pop
|
49
|
-
match_info = longest_match(from_start, from_end, to_start, to_end)
|
50
|
-
match_from_index, match_to_index, size = match_info
|
51
|
-
unless size.zero?
|
52
|
-
blocks << match_info
|
53
|
-
if from_start < match_from_index and to_start < match_to_index
|
54
|
-
queue.push([from_start, match_from_index,
|
55
|
-
to_start, match_to_index])
|
56
|
-
end
|
57
|
-
if match_from_index + size < from_end and
|
58
|
-
match_to_index + size < to_end
|
59
|
-
queue.push([match_from_index + size, from_end,
|
60
|
-
match_to_index + size, to_end])
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
non_adjacent = []
|
27
|
+
def blocks
|
28
|
+
_blocks = []
|
66
29
|
prev_from_index = prev_to_index = prev_size = 0
|
67
|
-
|
30
|
+
matches.sort.each do |from_index, to_index, size|
|
68
31
|
if prev_from_index + prev_size == from_index and
|
69
32
|
prev_to_index + prev_size == to_index
|
70
33
|
prev_size += size
|
71
34
|
else
|
72
35
|
unless prev_size.zero?
|
73
|
-
|
36
|
+
_blocks << [prev_from_index, prev_to_index, prev_size]
|
74
37
|
end
|
75
|
-
prev_from_index
|
76
|
-
|
38
|
+
prev_from_index = from_index
|
39
|
+
prev_to_index = to_index
|
40
|
+
prev_size = size
|
77
41
|
end
|
78
42
|
end
|
79
43
|
unless prev_size.zero?
|
80
|
-
|
44
|
+
_blocks << [prev_from_index, prev_to_index, prev_size]
|
81
45
|
end
|
82
46
|
|
83
|
-
|
84
|
-
|
47
|
+
_blocks << [@from.size, @to.size, 0]
|
48
|
+
_blocks
|
85
49
|
end
|
86
50
|
|
87
51
|
def operations
|
88
52
|
from_index = to_index = 0
|
89
53
|
operations = []
|
90
|
-
|
91
|
-
tag =
|
92
|
-
|
93
|
-
tag = :replace
|
94
|
-
elsif from_index < match_from_index
|
95
|
-
tag = :delete
|
96
|
-
elsif to_index < match_to_index
|
97
|
-
tag = :insert
|
98
|
-
end
|
99
|
-
|
54
|
+
blocks.each do |match_from_index, match_to_index, size|
|
55
|
+
tag = determine_tag(from_index, to_index,
|
56
|
+
match_from_index, match_to_index)
|
100
57
|
if tag
|
101
58
|
operations << [tag,
|
102
59
|
from_index, match_from_index,
|
@@ -110,16 +67,166 @@ module Test
|
|
110
67
|
match_to_index, to_index]
|
111
68
|
end
|
112
69
|
end
|
113
|
-
|
114
70
|
operations
|
115
71
|
end
|
116
72
|
|
73
|
+
def grouped_operations(context_size=nil)
|
74
|
+
context_size ||= 3
|
75
|
+
_operations = operations
|
76
|
+
_operations = [[:equal, 0, 0, 0, 0]] if _operations.empty?
|
77
|
+
expand_edge_equal_operations!(_operations, context_size)
|
78
|
+
|
79
|
+
group_window = context_size * 2
|
80
|
+
groups = []
|
81
|
+
group = []
|
82
|
+
_operations.each do |tag, from_start, from_end, to_start, to_end|
|
83
|
+
if tag == :equal and from_end - from_start > group_window
|
84
|
+
group << [tag,
|
85
|
+
from_start,
|
86
|
+
[from_end, from_start + context_size].min,
|
87
|
+
to_start,
|
88
|
+
[to_end, to_start + context_size].min]
|
89
|
+
groups << group
|
90
|
+
group = []
|
91
|
+
from_start = [from_start, from_end - context_size].max
|
92
|
+
to_start = [to_start, to_end - context_size].max
|
93
|
+
end
|
94
|
+
group << [tag, from_start, from_end, to_start, to_end]
|
95
|
+
end
|
96
|
+
groups << group unless group.empty?
|
97
|
+
groups
|
98
|
+
end
|
99
|
+
|
100
|
+
def ratio
|
101
|
+
matches = blocks.inject(0) {|result, block| result + block[-1]}
|
102
|
+
length = @from.length + @to.length
|
103
|
+
if length.zero?
|
104
|
+
1.0
|
105
|
+
else
|
106
|
+
2.0 * matches / length
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
117
110
|
private
|
118
111
|
def update_to_indexes
|
119
112
|
@to_indexes = {}
|
120
|
-
@
|
121
|
-
|
122
|
-
|
113
|
+
@junks = {}
|
114
|
+
each = @to.is_a?(String) ? :each_byte : :each
|
115
|
+
i = 0
|
116
|
+
@to.send(each) do |item|
|
117
|
+
@to_indexes[item] ||= []
|
118
|
+
@to_indexes[item] << i
|
119
|
+
i += 1
|
120
|
+
end
|
121
|
+
|
122
|
+
return if @junk_predicate.nil?
|
123
|
+
@to_indexes = @to_indexes.reject do |key, value|
|
124
|
+
junk = @junk_predicate.call(key)
|
125
|
+
@junks[key] = true if junk
|
126
|
+
junk
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def find_best_match_position(from_start, from_end, to_start, to_end)
|
131
|
+
best_from, best_to, best_size = from_start, to_start, 0
|
132
|
+
sizes = {}
|
133
|
+
from_start.upto(from_end) do |from_index|
|
134
|
+
_sizes = {}
|
135
|
+
(@to_indexes[@from[from_index]] || []).each do |to_index|
|
136
|
+
next if to_index < to_start
|
137
|
+
break if to_index > to_end
|
138
|
+
size = _sizes[to_index] = (sizes[to_index - 1] || 0) + 1
|
139
|
+
if size > best_size
|
140
|
+
best_from = from_index - size + 1
|
141
|
+
best_to = to_index - size + 1
|
142
|
+
best_size = size
|
143
|
+
end
|
144
|
+
end
|
145
|
+
sizes = _sizes
|
146
|
+
end
|
147
|
+
[best_from, best_to, best_size]
|
148
|
+
end
|
149
|
+
|
150
|
+
def adjust_best_info_with_junk_predicate(should_junk, best_info,
|
151
|
+
from_start, from_end,
|
152
|
+
to_start, to_end)
|
153
|
+
best_from, best_to, best_size = best_info
|
154
|
+
while best_from > from_start and best_to > to_start and
|
155
|
+
(should_junk ?
|
156
|
+
@junks.has_key?(@to[best_to]) :
|
157
|
+
!@junks.has_key?(@to[best_to])) and
|
158
|
+
@from[best_from] == @to[best_to]
|
159
|
+
best_from -= 1
|
160
|
+
best_to -= 1
|
161
|
+
best_size += 1
|
162
|
+
end
|
163
|
+
|
164
|
+
while best_from + best_size < from_end and
|
165
|
+
best_to + best_size < to_end and
|
166
|
+
(should_junk ?
|
167
|
+
@junks.has_key?(@to[best_to + best_size]) :
|
168
|
+
!@junks.has_key?(@to[best_to + best_size])) and
|
169
|
+
@from[best_from + best_size] == @to[best_to + best_size]
|
170
|
+
best_size += 1
|
171
|
+
end
|
172
|
+
|
173
|
+
[best_from, best_to, best_size]
|
174
|
+
end
|
175
|
+
|
176
|
+
def matches
|
177
|
+
_matches = []
|
178
|
+
queue = [[0, @from.size - 1, 0, @to.size - 1]]
|
179
|
+
until queue.empty?
|
180
|
+
from_start, from_end, to_start, to_end = queue.pop
|
181
|
+
match = longest_match(from_start, from_end, to_start, to_end)
|
182
|
+
match_from_index, match_to_index, size = match
|
183
|
+
unless size.zero?
|
184
|
+
_matches << match
|
185
|
+
if from_start < match_from_index and
|
186
|
+
to_start < match_to_index
|
187
|
+
queue.push([from_start, match_from_index - 1,
|
188
|
+
to_start, match_to_index - 1])
|
189
|
+
end
|
190
|
+
if match_from_index + size <= from_end and
|
191
|
+
match_to_index + size <= to_end
|
192
|
+
queue.push([match_from_index + size, from_end,
|
193
|
+
match_to_index + size, to_end])
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
_matches
|
198
|
+
end
|
199
|
+
|
200
|
+
def determine_tag(from_index, to_index,
|
201
|
+
match_from_index, match_to_index)
|
202
|
+
if from_index < match_from_index and to_index < match_to_index
|
203
|
+
:replace
|
204
|
+
elsif from_index < match_from_index
|
205
|
+
:delete
|
206
|
+
elsif to_index < match_to_index
|
207
|
+
:insert
|
208
|
+
else
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def expand_edge_equal_operations!(_operations, context_size)
|
214
|
+
tag, from_start, from_end, to_start, to_end = _operations[0]
|
215
|
+
if tag == :equal
|
216
|
+
_operations[0] = [tag,
|
217
|
+
[from_start, from_end - context_size].max,
|
218
|
+
from_end,
|
219
|
+
[to_start, to_end - context_size].max,
|
220
|
+
to_end]
|
221
|
+
end
|
222
|
+
|
223
|
+
tag, from_start, from_end, to_start, to_end = _operations[-1]
|
224
|
+
if tag == :equal
|
225
|
+
_operations[-1] = [tag,
|
226
|
+
from_start,
|
227
|
+
[from_end, from_start + context_size].min,
|
228
|
+
to_start,
|
229
|
+
[to_end, to_start + context_size].min]
|
123
230
|
end
|
124
231
|
end
|
125
232
|
end
|
@@ -130,20 +237,27 @@ module Test
|
|
130
237
|
@to = to
|
131
238
|
end
|
132
239
|
|
133
|
-
|
240
|
+
private
|
241
|
+
def tag(mark, contents)
|
242
|
+
contents.collect {|content| "#{mark}#{content}"}
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class ReadableDiffer < Differ
|
247
|
+
def diff(options={})
|
134
248
|
result = []
|
135
249
|
matcher = SequenceMatcher.new(@from, @to)
|
136
250
|
matcher.operations.each do |args|
|
137
251
|
tag, from_start, from_end, to_start, to_end = args
|
138
252
|
case tag
|
139
253
|
when :replace
|
140
|
-
result.concat(
|
254
|
+
result.concat(diff_lines(from_start, from_end, to_start, to_end))
|
141
255
|
when :delete
|
142
|
-
result.concat(
|
256
|
+
result.concat(tag_deleted(@from[from_start...from_end]))
|
143
257
|
when :insert
|
144
|
-
result.concat(
|
258
|
+
result.concat(tag_inserted(@to[to_start...to_end]))
|
145
259
|
when :equal
|
146
|
-
result.concat(
|
260
|
+
result.concat(tag_equal(@from[from_start...from_end]))
|
147
261
|
else
|
148
262
|
raise "unknown tag: #{tag}"
|
149
263
|
end
|
@@ -152,8 +266,98 @@ module Test
|
|
152
266
|
end
|
153
267
|
|
154
268
|
private
|
155
|
-
def
|
156
|
-
|
269
|
+
def tag_deleted(contents)
|
270
|
+
tag("- ", contents)
|
271
|
+
end
|
272
|
+
|
273
|
+
def tag_inserted(contents)
|
274
|
+
tag("+ ", contents)
|
275
|
+
end
|
276
|
+
|
277
|
+
def tag_equal(contents)
|
278
|
+
tag(" ", contents)
|
279
|
+
end
|
280
|
+
|
281
|
+
def diff_lines(from_start, from_end, to_start, to_end)
|
282
|
+
best_ratio, cut_off = 0.74, 0.75
|
283
|
+
from_equal_index = to_equal_index = nil
|
284
|
+
best_from_index = best_to_index = nil
|
285
|
+
to_start.upto(to_end - 1) do |to_index|
|
286
|
+
from_start.upto(from_end - 1) do |from_index|
|
287
|
+
if @from[from_index] == @to[to_index]
|
288
|
+
from_equal_index ||= from_index
|
289
|
+
to_equal_index ||= to_index
|
290
|
+
next
|
291
|
+
end
|
292
|
+
|
293
|
+
matcher = SequenceMatcher.new(@from[from_index], @to[to_index],
|
294
|
+
&method(:space_character?))
|
295
|
+
if matcher.ratio > best_ratio
|
296
|
+
best_ratio = matcher.ratio
|
297
|
+
best_from_index = from_index
|
298
|
+
best_to_index = to_index
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
if best_ratio < cut_off
|
304
|
+
if from_equal_index.nil?
|
305
|
+
tagged_from = tag_deleted(@from[from_start...from_end])
|
306
|
+
tagged_to = tag_inserted(@to[to_start...to_end])
|
307
|
+
if to_end - to_start < from_end - from_start
|
308
|
+
return tagged_to + tagged_from
|
309
|
+
else
|
310
|
+
return tagged_from + tagged_to
|
311
|
+
end
|
312
|
+
end
|
313
|
+
best_from_index = from_equal_index
|
314
|
+
best_to_index = to_equal_index
|
315
|
+
best_ratio = 1.0
|
316
|
+
else
|
317
|
+
from_equal_index = nil
|
318
|
+
end
|
319
|
+
|
320
|
+
_diff_lines(from_start, best_from_index, to_start, best_to_index) +
|
321
|
+
diff_line(@from[best_from_index], @to[best_to_index]) +
|
322
|
+
_diff_lines(best_from_index + 1, from_end, best_to_index + 1, to_end)
|
323
|
+
end
|
324
|
+
|
325
|
+
def _diff_lines(from_start, from_end, to_start, to_end)
|
326
|
+
if from_start < from_end
|
327
|
+
if to_start < to_end
|
328
|
+
diff_lines(from_start, from_end, to_start, to_end)
|
329
|
+
else
|
330
|
+
tag_deleted(@from[from_start...from_end])
|
331
|
+
end
|
332
|
+
else
|
333
|
+
tag_inserted(@to[to_start...to_end])
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def diff_line(from_line, to_line)
|
338
|
+
from_tags = ""
|
339
|
+
to_tags = ""
|
340
|
+
matcher = SequenceMatcher.new(from_line, to_line,
|
341
|
+
&method(:space_character?))
|
342
|
+
matcher.operations.each do |tag, from_start, from_end, to_start, to_end|
|
343
|
+
from_length = from_end - from_start
|
344
|
+
to_length = to_end - to_start
|
345
|
+
case tag
|
346
|
+
when :replace
|
347
|
+
from_tags << "^" * from_length
|
348
|
+
to_tags << "^" * to_length
|
349
|
+
when :delete
|
350
|
+
from_tags << "-" * from_length
|
351
|
+
when :insert
|
352
|
+
to_tags << "+" * to_length
|
353
|
+
when :equal
|
354
|
+
from_tags << " " * from_length
|
355
|
+
to_tags << " " * to_length
|
356
|
+
else
|
357
|
+
raise "unknown tag: #{tag}"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
format_diff_point(from_line, to_line, from_tags, to_tags)
|
157
361
|
end
|
158
362
|
|
159
363
|
def format_diff_point(from_line, to_line, from_tags, to_tags)
|
@@ -163,9 +367,9 @@ module Test
|
|
163
367
|
from_tags = from_tags[common..-1].rstrip
|
164
368
|
to_tags = to_tags[common..-1].rstrip
|
165
369
|
|
166
|
-
result = [
|
370
|
+
result = tag_deleted([from_line])
|
167
371
|
result << "? #{"\t" * common}#{from_tags}" unless from_tags.empty?
|
168
|
-
result
|
372
|
+
result.concat(tag_inserted([to_line]))
|
169
373
|
result << "? #{"\t" * common}#{to_tags}" unless to_tags.empty?
|
170
374
|
result
|
171
375
|
end
|
@@ -177,11 +381,99 @@ module Test
|
|
177
381
|
end
|
178
382
|
n
|
179
383
|
end
|
384
|
+
|
385
|
+
def space_character?(character)
|
386
|
+
[" "[0], "\t"[0]].include?(character)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
class UnifiedDiffer < Differ
|
391
|
+
def diff(options={})
|
392
|
+
groups = SequenceMatcher.new(@from, @to).grouped_operations
|
393
|
+
return [] if groups.empty?
|
394
|
+
return [] if same_content?(groups)
|
395
|
+
|
396
|
+
show_context = options[:show_context]
|
397
|
+
show_context = true if show_context.nil?
|
398
|
+
result = ["--- #{options[:from_label]}".rstrip,
|
399
|
+
"+++ #{options[:to_label]}".rstrip]
|
400
|
+
groups.each do |operations|
|
401
|
+
result << format_summary(operations, show_context)
|
402
|
+
operations.each do |args|
|
403
|
+
operation_tag, from_start, from_end, to_start, to_end = args
|
404
|
+
case operation_tag
|
405
|
+
when :replace
|
406
|
+
result.concat(tag("-", @from[from_start...from_end]))
|
407
|
+
result.concat(tag("+", @to[to_start...to_end]))
|
408
|
+
when :delete
|
409
|
+
result.concat(tag("-", @from[from_start...from_end]))
|
410
|
+
when :insert
|
411
|
+
result.concat(tag("+", @to[to_start...to_end]))
|
412
|
+
when :equal
|
413
|
+
result.concat(tag(" ", @from[from_start...from_end]))
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
result
|
418
|
+
end
|
419
|
+
|
420
|
+
private
|
421
|
+
def same_content?(groups)
|
422
|
+
return false if groups.size != 1
|
423
|
+
group = groups[0]
|
424
|
+
return false if group.size != 1
|
425
|
+
tag, from_start, from_end, to_start, to_end = group[0]
|
426
|
+
|
427
|
+
tag == :equal and [from_start, from_end] == [to_start, to_end]
|
428
|
+
end
|
429
|
+
|
430
|
+
def format_summary(operations, show_context)
|
431
|
+
_, first_from_start, _, first_to_start, _ = operations[0]
|
432
|
+
_, _, last_from_end, _, last_to_end = operations[-1]
|
433
|
+
summary = "@@ -%d,%d +%d,%d @@" % [first_from_start + 1,
|
434
|
+
last_from_end - first_from_start,
|
435
|
+
first_to_start + 1,
|
436
|
+
last_to_end - first_to_start,]
|
437
|
+
if show_context
|
438
|
+
interesting_line = find_interesting_line(first_from_start,
|
439
|
+
first_to_start,
|
440
|
+
:define_line?)
|
441
|
+
summary << " #{interesting_line}" if interesting_line
|
442
|
+
end
|
443
|
+
summary
|
444
|
+
end
|
445
|
+
|
446
|
+
def find_interesting_line(from_start, to_start, predicate)
|
447
|
+
from_index = from_start
|
448
|
+
to_index = to_start
|
449
|
+
while from_index >= 0 or to_index >= 0
|
450
|
+
[@from[from_index], @to[to_index]].each do |line|
|
451
|
+
return line if line and send(predicate, line)
|
452
|
+
end
|
453
|
+
|
454
|
+
from_index -= 1
|
455
|
+
to_index -= 1
|
456
|
+
end
|
457
|
+
nil
|
458
|
+
end
|
459
|
+
|
460
|
+
def define_line?(line)
|
461
|
+
/\A(?:[_a-zA-Z$]|\s*(?:class|module|def)\b)/ =~ line
|
462
|
+
end
|
180
463
|
end
|
181
464
|
|
182
465
|
module_function
|
183
|
-
def
|
184
|
-
|
466
|
+
def readable(from, to, options={})
|
467
|
+
diff(ReadableDiffer, from, to, options)
|
468
|
+
end
|
469
|
+
|
470
|
+
def unified(from, to, options={})
|
471
|
+
diff(UnifiedDiffer, from, to, options)
|
472
|
+
end
|
473
|
+
|
474
|
+
def diff(differ_class, from, to, options={})
|
475
|
+
differ = differ_class.new(from.split(/\r?\n/), to.split(/\r?\n/))
|
476
|
+
differ.diff(options).join("\n")
|
185
477
|
end
|
186
478
|
end
|
187
479
|
end
|