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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  = NEWS
4
4
 
5
+ == 0.2.0: 2008-03-03
6
+
7
+ * Supported XML report.
8
+ * Added --xml-report option.
9
+ * Supported diff output for assert_equal.
10
+
5
11
  == 0.1.0: 2008-02-21
6
12
 
7
13
  * Initial release.
data/NEWS.ja CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  = NEWS.ja
4
4
 
5
+ == 0.2.0: 2008-03-03
6
+
7
+ * XML出力のサポート。
8
+ * --xml-reportオプションの追加。
9
+ * assert_equal時のdiff出力のサポート。
10
+
5
11
  == 0.1.0: 2008-02-21
6
12
 
7
13
  * 最初のリリース。
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.1.0: 2008-02-21</a></h2><!-- RDLabel: "0.1.0: 2008-02-21" -->
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.1.0: 2008-02-21</a></h2><!-- RDLabel: "0.1.0: 2008-02-21" -->
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.1.0: 2008-02-21</a></h2><!-- RDLabel: "0.1.0: 2008-02-21" -->
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
@@ -9,3 +9,5 @@ require "test-unit-ext/priority"
9
9
  require "test-unit-ext/backtrace-filter"
10
10
  require "test-unit-ext/long-display-for-emacs"
11
11
  require "test-unit-ext/metadata"
12
+ require "test-unit-ext/xml-report"
13
+ require "test-unit-ext/assertions"
@@ -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
@@ -1,102 +1,59 @@
1
- # port of ndiff in Python's difflib.
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
- best_from, best_to, best_size = from_start, to_start, 0
14
- lengths = {}
15
- from_start.upto(from_end) do |i|
16
- new_lengths = {}
17
- (@to_indexes[@from[i]] || []).each do |j|
18
- next if j < to_start
19
- break if j >= to_end
20
- k = new_lengths[j] = (lengths[j - 1] || 0) + 1
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
- [best_from, best_to, best_size]
24
+ best_info
42
25
  end
43
26
 
44
- def matching_blocks
45
- queue = [[0, @from.size - 1, 0, @to.size - 1]]
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
- blocks.sort.each do |from_index, to_index, size|
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
- non_adjacent << [prev_from_index, prev_to_index, prev_size]
36
+ _blocks << [prev_from_index, prev_to_index, prev_size]
74
37
  end
75
- prev_from_index, prev_to_index, prev_size =
76
- from_index, to_index, size
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
- non_adjacent << [prev_from_index, prev_to_index, prev_size]
44
+ _blocks << [prev_from_index, prev_to_index, prev_size]
81
45
  end
82
46
 
83
- non_adjacent << [@from.size, @to.size, 0]
84
- non_adjacent
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
- matching_blocks.each do |match_from_index, match_to_index, size|
91
- tag = nil
92
- if from_index < match_from_index and to_index < match_to_index
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
- @to.each_with_index do |line, i|
121
- @to_indexes[line] ||= []
122
- @to_indexes[line] << i
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
- def compare
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(fancy_replace(from_start, from_end, to_start, to_end))
254
+ result.concat(diff_lines(from_start, from_end, to_start, to_end))
141
255
  when :delete
142
- result.concat(tagging('-', @from[from_start..from_end]))
256
+ result.concat(tag_deleted(@from[from_start...from_end]))
143
257
  when :insert
144
- result.concat(tagging('+', @to[to_start..to_end]))
258
+ result.concat(tag_inserted(@to[to_start...to_end]))
145
259
  when :equal
146
- result.concat(tagging(' ', @from[from_start..from_end]))
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 tagging(tag, contents)
156
- contents.collect {|content| "#{tag} #{content}"}
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 = ["- #{from_line}"]
370
+ result = tag_deleted([from_line])
167
371
  result << "? #{"\t" * common}#{from_tags}" unless from_tags.empty?
168
- result << "+ #{to_line}"
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 ndiff(from, to)
184
- Differ.new(from, to).compare.join("\n")
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