assert_same 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/LICENSE +19 -0
- data/README +5 -0
- data/Rakefile +25 -0
- data/assert_same.gemspec +20 -0
- data/init.rb +2 -0
- data/lib/array_diff.rb +331 -0
- data/lib/assert_same.rb +273 -0
- data/lib/text_diff.rb +109 -0
- data/test/assert_same_test.rb +18 -0
- data/test/logs/assert_same_with_files.log.ref +1 -0
- metadata +90 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010-2011 Pluron, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
Bundler::GemHelper.install_tasks
|
7
|
+
|
8
|
+
desc 'Default: run unit tests.'
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
desc 'Test assert_same.'
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'lib'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Generate documentation for assert_same.'
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
21
|
+
rdoc.title = 'assert_same'
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
data/assert_same.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
SPEC = Gem::Specification.new do |s|
|
4
|
+
s.name = "assert_same"
|
5
|
+
s.version = "0.1"
|
6
|
+
s.author = "Pluron, Inc."
|
7
|
+
s.email = "support@pluron.com"
|
8
|
+
s.homepage = "http://github.com/acunote/assert_same"
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.description = "assert_same assertion"
|
11
|
+
s.summary = "Assert which checks that two strings (expected and actual) are same and which can magically replace expected value with the actual in case the new behavior (and new actual value) is correct"
|
12
|
+
|
13
|
+
s.add_development_dependency('bundler', '>= 1.0.0')
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
17
|
+
|
18
|
+
s.require_path = "lib"
|
19
|
+
s.has_rdoc = true
|
20
|
+
end
|
data/init.rb
ADDED
data/lib/array_diff.rb
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
class ArrayDiff
|
2
|
+
|
3
|
+
VERSION = 0.3
|
4
|
+
|
5
|
+
def ArrayDiff.lcs(a, b)
|
6
|
+
astart = 0
|
7
|
+
bstart = 0
|
8
|
+
afinish = a.length-1
|
9
|
+
bfinish = b.length-1
|
10
|
+
mvector = []
|
11
|
+
|
12
|
+
# First we prune off any common elements at the beginning
|
13
|
+
while (astart <= afinish && bstart <= afinish && a[astart] == b[bstart])
|
14
|
+
mvector[astart] = bstart
|
15
|
+
astart += 1
|
16
|
+
bstart += 1
|
17
|
+
end
|
18
|
+
|
19
|
+
# now the end
|
20
|
+
while (astart <= afinish && bstart <= bfinish && a[afinish] == b[bfinish])
|
21
|
+
mvector[afinish] = bfinish
|
22
|
+
afinish -= 1
|
23
|
+
bfinish -= 1
|
24
|
+
end
|
25
|
+
|
26
|
+
bmatches = b.reverse_hash(bstart..bfinish)
|
27
|
+
thresh = []
|
28
|
+
links = []
|
29
|
+
|
30
|
+
(astart..afinish).each { |aindex|
|
31
|
+
aelem = a[aindex]
|
32
|
+
next unless bmatches.has_key? aelem
|
33
|
+
k = nil
|
34
|
+
bmatches[aelem].reverse.each { |bindex|
|
35
|
+
if k && (thresh[k] > bindex) && (thresh[k-1] < bindex)
|
36
|
+
thresh[k] = bindex
|
37
|
+
else
|
38
|
+
k = thresh.replacenextlarger(bindex, k)
|
39
|
+
end
|
40
|
+
links[k] = [ (k==0) ? nil : links[k-1], aindex, bindex ] if k
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
if !thresh.empty?
|
45
|
+
link = links[thresh.length-1]
|
46
|
+
while link
|
47
|
+
mvector[link[1]] = link[2]
|
48
|
+
link = link[0]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
return mvector
|
53
|
+
end
|
54
|
+
|
55
|
+
def makediff(a, b)
|
56
|
+
mvector = ArrayDiff.lcs(a, b)
|
57
|
+
ai = bi = 0
|
58
|
+
while ai < mvector.length
|
59
|
+
bline = mvector[ai]
|
60
|
+
if bline
|
61
|
+
while bi < bline
|
62
|
+
discardb(bi, b[bi])
|
63
|
+
bi += 1
|
64
|
+
end
|
65
|
+
match(ai, bi)
|
66
|
+
bi += 1
|
67
|
+
else
|
68
|
+
discarda(ai, a[ai])
|
69
|
+
end
|
70
|
+
ai += 1
|
71
|
+
end
|
72
|
+
while ai < a.length
|
73
|
+
discarda(ai, a[ai])
|
74
|
+
ai += 1
|
75
|
+
end
|
76
|
+
while bi < b.length
|
77
|
+
discardb(bi, b[bi])
|
78
|
+
bi += 1
|
79
|
+
end
|
80
|
+
match(ai, bi)
|
81
|
+
1
|
82
|
+
end
|
83
|
+
|
84
|
+
def compactdiffs
|
85
|
+
diffs = []
|
86
|
+
@diffs.each { |df|
|
87
|
+
i = 0
|
88
|
+
curdiff = []
|
89
|
+
while i < df.length
|
90
|
+
whot = df[i][0]
|
91
|
+
s = @isstring ? df[i][2].chr : [df[i][2]]
|
92
|
+
p = df[i][1]
|
93
|
+
last = df[i][1]
|
94
|
+
i += 1
|
95
|
+
while df[i] && df[i][0] == whot && df[i][1] == last+1
|
96
|
+
s << df[i][2]
|
97
|
+
last = df[i][1]
|
98
|
+
i += 1
|
99
|
+
end
|
100
|
+
curdiff.push [whot, p, s]
|
101
|
+
end
|
102
|
+
diffs.push curdiff
|
103
|
+
}
|
104
|
+
return diffs
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :diffs, :difftype
|
108
|
+
|
109
|
+
def initialize(diffs_or_a, b = nil, isstring = nil)
|
110
|
+
if b.nil?
|
111
|
+
@diffs = diffs_or_a
|
112
|
+
@isstring = isstring
|
113
|
+
else
|
114
|
+
@diffs = []
|
115
|
+
@curdiffs = []
|
116
|
+
makediff(diffs_or_a, b)
|
117
|
+
@difftype = diffs_or_a.class
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def match(ai, bi)
|
122
|
+
@diffs.push @curdiffs unless @curdiffs.empty?
|
123
|
+
@curdiffs = []
|
124
|
+
end
|
125
|
+
|
126
|
+
def discarda(i, elem)
|
127
|
+
@curdiffs.push ['-', i, elem]
|
128
|
+
end
|
129
|
+
|
130
|
+
def discardb(i, elem)
|
131
|
+
@curdiffs.push ['+', i, elem]
|
132
|
+
end
|
133
|
+
|
134
|
+
def compact
|
135
|
+
return Diff.new(compactdiffs)
|
136
|
+
end
|
137
|
+
|
138
|
+
def compact!
|
139
|
+
@diffs = compactdiffs
|
140
|
+
end
|
141
|
+
|
142
|
+
def inspect
|
143
|
+
@diffs.inspect
|
144
|
+
end
|
145
|
+
|
146
|
+
def diffrange(a, b)
|
147
|
+
if (a == b)
|
148
|
+
"#{a}"
|
149
|
+
else
|
150
|
+
"#{a},#{b}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_diff
|
155
|
+
offset = 0
|
156
|
+
@diffs.each { |b|
|
157
|
+
first = b[0][1]
|
158
|
+
length = b.length
|
159
|
+
action = b[0][0]
|
160
|
+
addcount = 0
|
161
|
+
remcount = 0
|
162
|
+
b.each { |l|
|
163
|
+
if l[0] == "+"
|
164
|
+
addcount += 1
|
165
|
+
elsif l[0] == "-"
|
166
|
+
remcount += 1
|
167
|
+
end
|
168
|
+
}
|
169
|
+
if addcount == 0
|
170
|
+
puts "#{diffrange(first+1, first+remcount)}d#{first+offset}"
|
171
|
+
elsif remcount == 0
|
172
|
+
puts "#{first-offset}a#{diffrange(first+1, first+addcount)}"
|
173
|
+
else
|
174
|
+
puts "#{diffrange(first+1, first+remcount)}c#{diffrange(first+offset+1, first+offset+addcount)}"
|
175
|
+
end
|
176
|
+
lastdel = (b[0][0] == "-")
|
177
|
+
b.each { |l|
|
178
|
+
if l[0] == "-"
|
179
|
+
offset -= 1
|
180
|
+
print "< "
|
181
|
+
elsif l[0] == "+"
|
182
|
+
offset += 1
|
183
|
+
if lastdel
|
184
|
+
lastdel = false
|
185
|
+
puts "---"
|
186
|
+
end
|
187
|
+
print "> "
|
188
|
+
end
|
189
|
+
print l[2]
|
190
|
+
print "\n"
|
191
|
+
}
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
module Diffable
|
199
|
+
def diff(b)
|
200
|
+
ArrayDiff.new(self, b)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Create a hash that maps elements of the array to arrays of indices
|
204
|
+
# where the elements are found.
|
205
|
+
|
206
|
+
def reverse_hash(range = (0...self.length))
|
207
|
+
revmap = {}
|
208
|
+
range.each { |i|
|
209
|
+
elem = self[i]
|
210
|
+
if revmap.has_key? elem
|
211
|
+
revmap[elem].push i
|
212
|
+
else
|
213
|
+
revmap[elem] = [i]
|
214
|
+
end
|
215
|
+
}
|
216
|
+
return revmap
|
217
|
+
end
|
218
|
+
|
219
|
+
def replacenextlarger(value, high = nil)
|
220
|
+
high ||= self.length
|
221
|
+
if self.empty? || value > self[-1]
|
222
|
+
push value
|
223
|
+
return high
|
224
|
+
end
|
225
|
+
# binary search for replacement point
|
226
|
+
low = 0
|
227
|
+
while low < high
|
228
|
+
index = (high+low)/2
|
229
|
+
found = self[index]
|
230
|
+
return nil if value == found
|
231
|
+
if value > found
|
232
|
+
low = index + 1
|
233
|
+
else
|
234
|
+
high = index
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
self[low] = value
|
239
|
+
# $stderr << "replace #{value} : 0/#{low}/#{init_high} (#{steps} steps) (#{init_high-low} off )\n"
|
240
|
+
# $stderr.puts self.inspect
|
241
|
+
#gets
|
242
|
+
#p length - low
|
243
|
+
return low
|
244
|
+
end
|
245
|
+
|
246
|
+
def patch(diff)
|
247
|
+
newary = nil
|
248
|
+
if diff.difftype == String
|
249
|
+
newary = diff.difftype.new('')
|
250
|
+
else
|
251
|
+
newary = diff.difftype.new
|
252
|
+
end
|
253
|
+
ai = 0
|
254
|
+
bi = 0
|
255
|
+
diff.diffs.each { |d|
|
256
|
+
d.each { |mod|
|
257
|
+
case mod[0]
|
258
|
+
when '-'
|
259
|
+
while ai < mod[1]
|
260
|
+
newary << self[ai]
|
261
|
+
ai += 1
|
262
|
+
bi += 1
|
263
|
+
end
|
264
|
+
ai += 1
|
265
|
+
when '+'
|
266
|
+
while bi < mod[1]
|
267
|
+
newary << self[ai]
|
268
|
+
ai += 1
|
269
|
+
bi += 1
|
270
|
+
end
|
271
|
+
newary << mod[2]
|
272
|
+
bi += 1
|
273
|
+
else
|
274
|
+
raise "Unknown diff action"
|
275
|
+
end
|
276
|
+
}
|
277
|
+
}
|
278
|
+
while ai < self.length
|
279
|
+
newary << self[ai]
|
280
|
+
ai += 1
|
281
|
+
bi += 1
|
282
|
+
end
|
283
|
+
return newary
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class Array
|
288
|
+
include Diffable
|
289
|
+
end
|
290
|
+
|
291
|
+
class String
|
292
|
+
include Diffable
|
293
|
+
end
|
294
|
+
|
295
|
+
=begin
|
296
|
+
= ArrayDiff
|
297
|
+
(({diff.rb})) - computes the differences between two arrays or
|
298
|
+
strings. Copyright (C) 2001 Lars Christensen
|
299
|
+
http://users.cybercity.dk/~dsl8950/ruby/diff.html
|
300
|
+
|
301
|
+
== Synopsis
|
302
|
+
|
303
|
+
diff = ArrayDiff.new(a, b)
|
304
|
+
b = a.patch(diff)
|
305
|
+
|
306
|
+
== Class ArrayDiff
|
307
|
+
=== Class Methods
|
308
|
+
--- ArrayDiff.new(a, b)
|
309
|
+
--- a.diff(b)
|
310
|
+
Creates a Diff object which represent the differences between
|
311
|
+
((|a|)) and ((|b|)). ((|a|)) and ((|b|)) can be either be arrays
|
312
|
+
of any objects, strings, or object of any class that include
|
313
|
+
module ((|Diffable|))
|
314
|
+
|
315
|
+
== Module Diffable
|
316
|
+
The module ((|Diffable|)) is intended to be included in any class for
|
317
|
+
which differences are to be computed. Diffable is included into String
|
318
|
+
and Array when (({diff.rb})) is (({require}))'d.
|
319
|
+
|
320
|
+
Classes including Diffable should implement (({[]})) to get element at
|
321
|
+
integer indices, (({<<})) to append elements to the object and
|
322
|
+
(({ClassName#new})) should accept 0 arguments to create a new empty
|
323
|
+
object.
|
324
|
+
|
325
|
+
=== Instance Methods
|
326
|
+
--- Diffable#patch(diff)
|
327
|
+
Applies the differences from ((|diff|)) to the object ((|obj|))
|
328
|
+
and return the result. ((|obj|)) is not changed. ((|obj|)) and
|
329
|
+
can be either an array or a string, but must match the object
|
330
|
+
from which the ((|diff|)) was created.
|
331
|
+
=end
|
data/lib/assert_same.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
# Copyright (c) 2010-2011 Pluron, Inc.
|
2
|
+
require 'test/unit'
|
3
|
+
require 'text_diff'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
class Test::Unit::TestCase
|
7
|
+
|
8
|
+
#Hash[filename][line_number] = offset
|
9
|
+
#For each line in the original file we store its offset (+N or -N lines)
|
10
|
+
#relative to the actual file
|
11
|
+
@@file_offsets = Hash.new { |hash, key| hash[key] = {} }
|
12
|
+
|
13
|
+
# assert_same: assert which checks that two strings (expected and actual) are same
|
14
|
+
# and which can "magically" replace expected value with the actual in case
|
15
|
+
# the new behavior (and new actual value) is correct
|
16
|
+
#
|
17
|
+
# == Usage ==
|
18
|
+
#
|
19
|
+
# Write this in the test source:
|
20
|
+
# assert_same something, <<-END
|
21
|
+
# foo
|
22
|
+
# bar
|
23
|
+
# zee
|
24
|
+
# END
|
25
|
+
#
|
26
|
+
# Then run tests as usual:
|
27
|
+
# rake test:units
|
28
|
+
# ruby test/unit/foo_test.rb
|
29
|
+
# ...
|
30
|
+
#
|
31
|
+
# When assert_same fails, you'll be able to:
|
32
|
+
# - review diff
|
33
|
+
# - (optionally) accept new actual value (this modifies the test source file)
|
34
|
+
#
|
35
|
+
# Additional options for test runs:
|
36
|
+
# --no-interactive skips all questions and just reports failures
|
37
|
+
# --autoaccept prints diffs and automatically accepts all new actual values
|
38
|
+
# --no-canonicalize turns off expected and actual value canonicalization (see below for details)
|
39
|
+
#
|
40
|
+
# Additional options can be passed during both single test file run and rake test run:
|
41
|
+
# ruby test/unit/foo_test.rb -- --autoaccept
|
42
|
+
# ruby test/unit/foo_test.rb -- --no-interactive
|
43
|
+
#
|
44
|
+
# rake test TESTOPTS="-- --autoaccept"
|
45
|
+
# rake test:units TESTOPTS="-- --no-canonicalize --autoaccept"
|
46
|
+
#
|
47
|
+
#
|
48
|
+
#
|
49
|
+
# == Canonicalization ==
|
50
|
+
#
|
51
|
+
# Before comparing expected and actual strings, assert_same canonicalizes both using these rules:
|
52
|
+
# - indentation is ignored (except for indentation
|
53
|
+
# relative to the first line of the expected/actual string)
|
54
|
+
# - ruby-style comments after "#" are ignored:
|
55
|
+
# both whole-line and end-of-line comments are supported
|
56
|
+
# - empty lines are ignored
|
57
|
+
# - trailing whitespaces are ignored
|
58
|
+
#
|
59
|
+
# You can turn canonicalization off with --no-canonicalize option. This is useful
|
60
|
+
# when you need to regenerate expected test strings.
|
61
|
+
# To regenerate the whole test suite, run:
|
62
|
+
# rake test TESTOPTS="-- --no-canonicalize --autoaccept"
|
63
|
+
#
|
64
|
+
# Example of assert_same with comments:
|
65
|
+
# assert_same something, <<-END
|
66
|
+
# # some tree
|
67
|
+
# foo 1
|
68
|
+
# foo 1.1
|
69
|
+
# foo 1.2 # some node
|
70
|
+
# bar 2
|
71
|
+
# bar 2.1
|
72
|
+
# END
|
73
|
+
#
|
74
|
+
#
|
75
|
+
#
|
76
|
+
# == Umportant Usage Rules ==
|
77
|
+
#
|
78
|
+
# Restrictions:
|
79
|
+
# - only END and EOS are supported as end of string sequence
|
80
|
+
# - it's a requirement that you have <<-END at the same line as assert_same
|
81
|
+
# - assert_same can't be within a block
|
82
|
+
#
|
83
|
+
# Storing expected output in files:
|
84
|
+
# - assert_same something, :log => <path_to_file>
|
85
|
+
# - path to file is relative to:
|
86
|
+
# - RAILS_ROOT (if that is defined)
|
87
|
+
# - current dir (if no RAILS_ROOT is defined)
|
88
|
+
# - file doesn't have to exist, it will be created if necessary
|
89
|
+
#
|
90
|
+
# Misc:
|
91
|
+
# - it's ok to have several assert_same's in the same test method, assert_same.
|
92
|
+
# correctly updates all assert_same's in the test file
|
93
|
+
# - it's ok to omit expected string, like this:
|
94
|
+
# assert_same something
|
95
|
+
# in fact, this is the preferred way to create assert_same tests - you write empty
|
96
|
+
# assert_same, run tests and they will fill expected values for you automatically
|
97
|
+
def assert_same(actual, expected = :autofill_expected_value)
|
98
|
+
if expected.class == String
|
99
|
+
expected ||= ""
|
100
|
+
mode = :expecting_string
|
101
|
+
elsif expected == :autofill_expected_value
|
102
|
+
expected = ""
|
103
|
+
mode = :autofill_expected_value
|
104
|
+
elsif expected.class == Hash
|
105
|
+
raise ":log key is missing" unless expected.has_key? :log
|
106
|
+
mode = :expecting_file
|
107
|
+
log_file = expected[:log]
|
108
|
+
if defined? RAILS_ROOT
|
109
|
+
log_file = File.expand_path(log_file, RAILS_ROOT)
|
110
|
+
else
|
111
|
+
log_file = File.expand_path(log_file, Dir.pwd)
|
112
|
+
end
|
113
|
+
expected = File.exists?(log_file) ? File.read(log_file) : ""
|
114
|
+
else
|
115
|
+
internal_error("Incorrect expected argument for assert_same. It must be either String or Hash.")
|
116
|
+
end
|
117
|
+
|
118
|
+
# interactive mode is turned on by default, except when
|
119
|
+
# - --no-interactive is given
|
120
|
+
# - STDIN is not a terminal device (i.e. we can't ask any questions)
|
121
|
+
interactive = !ARGV.include?("--no-interactive") && STDIN.tty?
|
122
|
+
canonicalize = !ARGV.include?("--no-canonicalize")
|
123
|
+
autoaccept = ARGV.include?("--autoaccept")
|
124
|
+
|
125
|
+
is_same_canonicalized, is_same, diff_canonicalized, diff = compare_for_assert_same(expected, actual)
|
126
|
+
|
127
|
+
if (canonicalize and !is_same_canonicalized) or (!canonicalize and !is_same)
|
128
|
+
diff_to_report = canonicalize ? diff_canonicalized : diff
|
129
|
+
if interactive
|
130
|
+
# print method name and short backtrace
|
131
|
+
failure = Test::Unit::Failure.new(name, filter_backtrace(caller(0)), diff_to_report)
|
132
|
+
puts "\n#{failure}"
|
133
|
+
|
134
|
+
if autoaccept
|
135
|
+
accept = true
|
136
|
+
else
|
137
|
+
print "Accept the new value: yes to all, no to all, yes, no? [Y/N/y/n] (y): "
|
138
|
+
STDOUT.flush
|
139
|
+
response = STDIN.gets.strip
|
140
|
+
accept = ["", "y", "Y"].include? response
|
141
|
+
ARGV << "--autoaccept" if response == "Y"
|
142
|
+
ARGV << "--no-interactive" if response == "N"
|
143
|
+
end
|
144
|
+
|
145
|
+
if accept
|
146
|
+
if [:expecting_string, :autofill_expected_value].include? mode
|
147
|
+
accept_string(actual, mode)
|
148
|
+
elsif mode == :expecting_file
|
149
|
+
accept_file(actual, log_file)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
if accept
|
154
|
+
# when change is accepted, we should not report it as a failure because
|
155
|
+
# we want the test method to continue executing (in case there're more
|
156
|
+
# assert_same's in the method)
|
157
|
+
add_assertion
|
158
|
+
else
|
159
|
+
raise Test::Unit::AssertionFailedError.new(diff_to_report)
|
160
|
+
end
|
161
|
+
else
|
162
|
+
add_assertion
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def accept_string(actual, mode)
|
169
|
+
file, method, line = get_caller_location(:depth => 3)
|
170
|
+
|
171
|
+
# read source file, construct the new source, replacing everything
|
172
|
+
# between "do" and "end" in assert_same's block
|
173
|
+
# using File::expand_path here because "file" can be either
|
174
|
+
# absolute path (when test is run with "rake test" runs)
|
175
|
+
# or relative path (when test is run via ruby <path_to_test_file>)
|
176
|
+
source = File.readlines(File::expand_path(file))
|
177
|
+
|
178
|
+
# file may be changed by previous accepted assert_same's, adjust line numbers
|
179
|
+
offset = @@file_offsets[file].keys.inject(0) do |sum, i|
|
180
|
+
line.to_i >= i ? sum + @@file_offsets[file][i] : sum
|
181
|
+
end
|
182
|
+
|
183
|
+
expected_text_end_line = expected_text_start_line = line.to_i + offset
|
184
|
+
unless mode == :autofill_expected_value
|
185
|
+
#if we're autofilling the value, END/EOS marker will not exist
|
186
|
+
#(second arg to assert_same is omitted)
|
187
|
+
#else we search for it
|
188
|
+
expected_text_end_line += 1 while !["END", "EOS"].include?(source[expected_text_end_line].strip)
|
189
|
+
end
|
190
|
+
|
191
|
+
expected_length = expected_text_end_line - expected_text_start_line
|
192
|
+
|
193
|
+
# indentation is the indentation of assert_same call + 4
|
194
|
+
indentation = source[expected_text_start_line-1] =~ /^(\s+)/ ? $1.length : 0
|
195
|
+
indentation += 4
|
196
|
+
|
197
|
+
if mode == :autofill_expected_value
|
198
|
+
# add second argument to assert_same if it's omitted
|
199
|
+
source[expected_text_start_line-1] = "#{source[expected_text_start_line-1].chop}, <<-END\n"
|
200
|
+
end
|
201
|
+
source = source[0, expected_text_start_line] +
|
202
|
+
actual.split("\n").map { |l| "#{" "*(indentation)}#{l}\n"} +
|
203
|
+
(mode == :autofill_expected_value ? ["#{" "*(indentation-4)}END\n"] : [])+
|
204
|
+
source[expected_text_end_line, source.length]
|
205
|
+
|
206
|
+
# recalculate line number adjustments
|
207
|
+
actual_length = actual.split("\n").length
|
208
|
+
actual_length += 1 if mode == :autofill_expected_value # END marker after actual value
|
209
|
+
@@file_offsets[file][line.to_i] = actual_length - expected_length
|
210
|
+
|
211
|
+
source_file = File.open(file, "w+")
|
212
|
+
source_file.write(source)
|
213
|
+
source_file.fsync
|
214
|
+
source_file.close
|
215
|
+
end
|
216
|
+
|
217
|
+
def accept_file(actual, log_file)
|
218
|
+
log = File.open(log_file, "w+")
|
219
|
+
log.write(actual)
|
220
|
+
log.fsync
|
221
|
+
log.close
|
222
|
+
end
|
223
|
+
|
224
|
+
def compare_for_assert_same(expected_verbatim, actual_verbatim)
|
225
|
+
expected_canonicalized, expected = canonicalize_for_assert_same(expected_verbatim)
|
226
|
+
actual_canonicalized, actual = canonicalize_for_assert_same(actual_verbatim)
|
227
|
+
diff_canonicalized = AssertSame::TextDiff.array_diff(expected_canonicalized, actual_canonicalized)
|
228
|
+
diff = AssertSame::TextDiff.array_diff(expected, actual)
|
229
|
+
[expected_canonicalized == actual_canonicalized, expected == actual, diff_canonicalized, diff]
|
230
|
+
end
|
231
|
+
|
232
|
+
def canonicalize_for_assert_same(text)
|
233
|
+
# make array of lines out of the text
|
234
|
+
result = text.split("\n")
|
235
|
+
|
236
|
+
# ignore leading newlines if any (trailing will be automatically ignored by split())
|
237
|
+
result.delete_at(0) if result[0] == ""
|
238
|
+
|
239
|
+
indentation = $1.length if result[0] and result[0] =~ /^(\s+)/
|
240
|
+
|
241
|
+
result.map! do |line|
|
242
|
+
# ignore indentation: we assume that the first line defines indentation
|
243
|
+
line.gsub!(/^\s{#{indentation}}/, '') if indentation
|
244
|
+
# ignore trailing spaces
|
245
|
+
line.gsub(/\s*$/, '')
|
246
|
+
end
|
247
|
+
|
248
|
+
# ignore comments
|
249
|
+
result_canonicalized= result.map do |line|
|
250
|
+
line.gsub(/\s*(#.*)?$/, '')
|
251
|
+
end
|
252
|
+
# ignore blank lines (usually they are lines with comments only)
|
253
|
+
result_canonicalized.delete_if { |line| line.nil? or line.empty? }
|
254
|
+
|
255
|
+
[result_canonicalized, result]
|
256
|
+
end
|
257
|
+
|
258
|
+
def get_caller_location(options = {:depth => 2})
|
259
|
+
caller_method = caller(options[:depth])[0]
|
260
|
+
|
261
|
+
#Sample output is:
|
262
|
+
#either full path when run as "rake test"
|
263
|
+
# /home/user/assert_same/test/unit/assure_test.rb:9:in `test_assure
|
264
|
+
#or relative path when run as ruby test/unit/assure_test.rb
|
265
|
+
# test/unit/assure_test.rb:9:in `test_assure
|
266
|
+
caller_method =~ /([^:]+):([0-9]+):in `(.+)'/
|
267
|
+
file = $1
|
268
|
+
line = $2
|
269
|
+
method = $3
|
270
|
+
[file, method, line]
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
data/lib/text_diff.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Copyright (c) 2008 Pluron, Inc.
|
2
|
+
|
3
|
+
require 'array_diff'
|
4
|
+
|
5
|
+
module AssertSame
|
6
|
+
|
7
|
+
class TextDiff
|
8
|
+
|
9
|
+
def self.mapped_chunk(arr, from, to)
|
10
|
+
return [] if to < from
|
11
|
+
arr[from, to-from+1].map{|x| [' ', x]}
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.chunk(arr, from, to)
|
15
|
+
result = []
|
16
|
+
if to - from > 2
|
17
|
+
result += self.mapped_chunk(arr, from, from+2) if from > 0
|
18
|
+
result << ['@', to-2]
|
19
|
+
result += self.mapped_chunk(arr, to-2, to)
|
20
|
+
else
|
21
|
+
result << ['@', from] if from == 0
|
22
|
+
result += self.mapped_chunk(arr, from, to)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.record_stat(arr, at, remcount, addcount, linecount, offset)
|
27
|
+
index = arr[at][1]+1
|
28
|
+
['', "@@ -#{index},#{linecount-addcount}, +#{index+offset},#{linecount-remcount} @@"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.diff(a_text, b_text)
|
32
|
+
a = a_text.split("\n")
|
33
|
+
b = b_text.split("\n")
|
34
|
+
self.array_diff(a, b)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.array_diff(a, b)
|
38
|
+
diff = ArrayDiff.new(a, b)
|
39
|
+
|
40
|
+
return nil if diff.diffs.empty?
|
41
|
+
|
42
|
+
result = []
|
43
|
+
|
44
|
+
from = to = nextfrom = 0
|
45
|
+
offset = 0
|
46
|
+
diff.diffs.each do |tuple|
|
47
|
+
first = tuple[0][1]
|
48
|
+
length = tuple.length
|
49
|
+
action = tuple[0][0]
|
50
|
+
addcount = 0
|
51
|
+
remcount = 0
|
52
|
+
tuple.each do |l|
|
53
|
+
if l[0] == "+"
|
54
|
+
addcount += 1
|
55
|
+
elsif l[0] == "-"
|
56
|
+
remcount += 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
if remcount == 0
|
60
|
+
to = first-offset-1
|
61
|
+
nextfrom = to+1
|
62
|
+
else
|
63
|
+
to = first-1
|
64
|
+
nextfrom = to+remcount+1
|
65
|
+
end
|
66
|
+
result += self.chunk(a, from, to)
|
67
|
+
from = nextfrom
|
68
|
+
lastdel = (tuple[0][0] == "-")
|
69
|
+
tuple.each do |l|
|
70
|
+
if l[0] == "-"
|
71
|
+
offset -= 1
|
72
|
+
else
|
73
|
+
offset += 1
|
74
|
+
end
|
75
|
+
result << [l[0], l[2]]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
if (a.length - from) > 2
|
79
|
+
result += self.chunk(a, from, from+2)
|
80
|
+
elsif a.length > 0
|
81
|
+
result += self.chunk(a, from, a.length-1)
|
82
|
+
end
|
83
|
+
linecount = addcount = remcount = offset = current_offset = 0
|
84
|
+
info_index = nil
|
85
|
+
result.each_with_index do |l, i|
|
86
|
+
if l[0] == '@'
|
87
|
+
result[info_index] = self.record_stat(result, info_index, remcount, addcount,
|
88
|
+
linecount, offset) if info_index
|
89
|
+
info_index = i
|
90
|
+
offset += addcount - remcount
|
91
|
+
linecount = 0
|
92
|
+
addcount = remcount = 0
|
93
|
+
elsif l[0] == '-'
|
94
|
+
remcount += 1
|
95
|
+
linecount += 1
|
96
|
+
elsif l[0] == '+'
|
97
|
+
addcount += 1
|
98
|
+
linecount += 1
|
99
|
+
else
|
100
|
+
linecount += 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
result[info_index] = self.record_stat(result, info_index, remcount, addcount, linecount, offset) if info_index
|
104
|
+
return result.map{|x| x.join('')}.join("\n")
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright (c) 2011 Pluron, Inc.
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'lib/assert_same'
|
5
|
+
|
6
|
+
class AssertSameTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def test_basic_assert_same
|
9
|
+
assert_same "foo", <<-END
|
10
|
+
foo
|
11
|
+
END
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_assert_same_with_files
|
15
|
+
assert_same "foo", :log => 'test/logs/assert_same_with_files.log.ref'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
foo
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: assert_same
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Pluron, Inc.
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-03-16 00:00:00 Z
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: bundler
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 23
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 0
|
31
|
+
- 0
|
32
|
+
version: 1.0.0
|
33
|
+
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
description: assert_same assertion
|
36
|
+
email: support@pluron.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- LICENSE
|
46
|
+
- README
|
47
|
+
- Rakefile
|
48
|
+
- assert_same.gemspec
|
49
|
+
- init.rb
|
50
|
+
- lib/array_diff.rb
|
51
|
+
- lib/assert_same.rb
|
52
|
+
- lib/text_diff.rb
|
53
|
+
- test/assert_same_test.rb
|
54
|
+
- test/logs/assert_same_with_files.log.ref
|
55
|
+
homepage: http://github.com/acunote/assert_same
|
56
|
+
licenses: []
|
57
|
+
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
hash: 3
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.8.15
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: Assert which checks that two strings (expected and actual) are same and which can magically replace expected value with the actual in case the new behavior (and new actual value) is correct
|
88
|
+
test_files:
|
89
|
+
- test/assert_same_test.rb
|
90
|
+
- test/logs/assert_same_with_files.log.ref
|