test-unit-ext 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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