minispec 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.pryrc +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +2140 -0
- data/Rakefile +11 -0
- data/bin/minispec +4 -0
- data/lib/minispec.rb +175 -0
- data/lib/minispec/api.rb +2 -0
- data/lib/minispec/api/class.rb +195 -0
- data/lib/minispec/api/class/after.rb +49 -0
- data/lib/minispec/api/class/around.rb +54 -0
- data/lib/minispec/api/class/before.rb +101 -0
- data/lib/minispec/api/class/helpers.rb +116 -0
- data/lib/minispec/api/class/let.rb +44 -0
- data/lib/minispec/api/class/tests.rb +33 -0
- data/lib/minispec/api/instance.rb +158 -0
- data/lib/minispec/api/instance/mocks/doubles.rb +36 -0
- data/lib/minispec/api/instance/mocks/mocks.rb +319 -0
- data/lib/minispec/api/instance/mocks/spies.rb +17 -0
- data/lib/minispec/api/instance/mocks/stubs.rb +105 -0
- data/lib/minispec/helpers.rb +1 -0
- data/lib/minispec/helpers/array.rb +56 -0
- data/lib/minispec/helpers/booleans.rb +108 -0
- data/lib/minispec/helpers/generic.rb +24 -0
- data/lib/minispec/helpers/mocks/expectations.rb +29 -0
- data/lib/minispec/helpers/mocks/spies.rb +36 -0
- data/lib/minispec/helpers/raise.rb +44 -0
- data/lib/minispec/helpers/throw.rb +29 -0
- data/lib/minispec/mocks.rb +11 -0
- data/lib/minispec/mocks/expectations.rb +77 -0
- data/lib/minispec/mocks/stubs.rb +178 -0
- data/lib/minispec/mocks/validations.rb +80 -0
- data/lib/minispec/mocks/validations/amount.rb +63 -0
- data/lib/minispec/mocks/validations/arguments.rb +161 -0
- data/lib/minispec/mocks/validations/caller.rb +43 -0
- data/lib/minispec/mocks/validations/order.rb +47 -0
- data/lib/minispec/mocks/validations/raise.rb +111 -0
- data/lib/minispec/mocks/validations/return.rb +74 -0
- data/lib/minispec/mocks/validations/throw.rb +91 -0
- data/lib/minispec/mocks/validations/yield.rb +141 -0
- data/lib/minispec/proxy.rb +201 -0
- data/lib/minispec/reporter.rb +185 -0
- data/lib/minispec/utils.rb +139 -0
- data/lib/minispec/utils/differ.rb +325 -0
- data/lib/minispec/utils/pretty_print.rb +51 -0
- data/lib/minispec/utils/raise.rb +123 -0
- data/lib/minispec/utils/throw.rb +140 -0
- data/minispec.gemspec +27 -0
- data/test/mocks/expectations/amount.rb +67 -0
- data/test/mocks/expectations/arguments.rb +126 -0
- data/test/mocks/expectations/caller.rb +55 -0
- data/test/mocks/expectations/generic.rb +35 -0
- data/test/mocks/expectations/order.rb +46 -0
- data/test/mocks/expectations/raise.rb +166 -0
- data/test/mocks/expectations/return.rb +71 -0
- data/test/mocks/expectations/throw.rb +113 -0
- data/test/mocks/expectations/yield.rb +109 -0
- data/test/mocks/spies/amount.rb +68 -0
- data/test/mocks/spies/arguments.rb +57 -0
- data/test/mocks/spies/generic.rb +61 -0
- data/test/mocks/spies/order.rb +38 -0
- data/test/mocks/spies/raise.rb +158 -0
- data/test/mocks/spies/return.rb +71 -0
- data/test/mocks/spies/throw.rb +113 -0
- data/test/mocks/spies/yield.rb +101 -0
- data/test/mocks/test__doubles.rb +98 -0
- data/test/mocks/test__expectations.rb +27 -0
- data/test/mocks/test__mocks.rb +197 -0
- data/test/mocks/test__proxies.rb +61 -0
- data/test/mocks/test__spies.rb +43 -0
- data/test/mocks/test__stubs.rb +427 -0
- data/test/proxified_asserts.rb +34 -0
- data/test/setup.rb +53 -0
- data/test/test__around.rb +58 -0
- data/test/test__assert.rb +510 -0
- data/test/test__before_and_after.rb +117 -0
- data/test/test__before_and_after_all.rb +71 -0
- data/test/test__helpers.rb +197 -0
- data/test/test__raise.rb +104 -0
- data/test/test__skip.rb +41 -0
- data/test/test__throw.rb +103 -0
- metadata +196 -0
@@ -0,0 +1,185 @@
|
|
1
|
+
module MiniSpec
|
2
|
+
class Reporter
|
3
|
+
@@indent = " ".freeze
|
4
|
+
|
5
|
+
attr_reader :failed_specs, :failed_tests, :skipped_tests
|
6
|
+
|
7
|
+
def initialize stdout = STDOUT
|
8
|
+
@stdout = stdout
|
9
|
+
@failed_specs, @failed_tests, @skipped_tests = [], {}, {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def summary
|
13
|
+
summary__failed_specs
|
14
|
+
summary__failed_tests
|
15
|
+
summary__skipped_tests
|
16
|
+
totals
|
17
|
+
end
|
18
|
+
|
19
|
+
def summary__failed_specs
|
20
|
+
return if @failed_specs.empty?
|
21
|
+
puts
|
22
|
+
puts(error('--- Failed Specs ---'))
|
23
|
+
last_ex = nil
|
24
|
+
@failed_specs.each do |(spec,proc,ex)|
|
25
|
+
puts(info(spec))
|
26
|
+
puts(info('defined at ' + proc.source_location.join(':')), indent: 2) if proc.is_a?(Proc)
|
27
|
+
if last_ex && ex.backtrace == last_ex.backtrace
|
28
|
+
puts('see exception above', indent: 2)
|
29
|
+
next
|
30
|
+
end
|
31
|
+
last_ex = ex
|
32
|
+
puts(error(ex.message), indent: 2)
|
33
|
+
ex.backtrace.each {|l| puts(l, indent: 2)}
|
34
|
+
puts
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def summary__skipped_tests
|
39
|
+
return if @skipped_tests.empty?
|
40
|
+
puts
|
41
|
+
puts(warn('--- Skipped Tests ---'))
|
42
|
+
@skipped_tests.each_pair do |spec,tests|
|
43
|
+
puts(info(spec))
|
44
|
+
tests.each do |(test,source_location)|
|
45
|
+
puts(warn(test), indent: 2)
|
46
|
+
puts(info(MiniSpec::Utils.shorten_source(source_location)), indent: 2)
|
47
|
+
puts
|
48
|
+
end
|
49
|
+
puts
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def summary__failed_tests
|
54
|
+
return if @failed_tests.empty?
|
55
|
+
puts
|
56
|
+
puts(error('--- Failed Tests ---'), '')
|
57
|
+
@failed_tests.each_pair do |spec, failures|
|
58
|
+
@failed_specs.push(spec) # to be used on #totals__specs
|
59
|
+
failures.each do |(test,verb,proc,errors)|
|
60
|
+
errors.each do |error|
|
61
|
+
error.is_a?(Exception) ?
|
62
|
+
exception_details(spec, test, error) :
|
63
|
+
failure_details(spec, test, error)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def totals
|
70
|
+
puts
|
71
|
+
puts('---')
|
72
|
+
totals__specs
|
73
|
+
totals__tests
|
74
|
+
totals__assertions
|
75
|
+
end
|
76
|
+
|
77
|
+
def totals__specs
|
78
|
+
print(info(' Specs: '))
|
79
|
+
return puts(success(Minispec.specs.size)) if @failed_specs.empty?
|
80
|
+
print(info(Minispec.specs.size))
|
81
|
+
puts(error(' (%s failed)' % @failed_specs.size))
|
82
|
+
end
|
83
|
+
|
84
|
+
def totals__tests
|
85
|
+
print(info(' Tests: '))
|
86
|
+
print(send(@failed_tests.any? ? :info : :success, Minispec.tests))
|
87
|
+
failed = error('%s failed' % @failed_tests.values.map(&:size).reduce(:+)) if @failed_tests.any?
|
88
|
+
skipped = warn('%s skipped' % @skipped_tests.size) if @skipped_tests.any?
|
89
|
+
report = [failed, skipped].compact.join(', ')
|
90
|
+
puts(report.empty? ? report : ' (%s)' % report)
|
91
|
+
end
|
92
|
+
|
93
|
+
def totals__assertions
|
94
|
+
print(info(' Assertions: '))
|
95
|
+
return puts(success(Minispec.assertions)) if @failed_tests.empty?
|
96
|
+
print(info(Minispec.assertions))
|
97
|
+
puts(error(' (%s failed)' % @failed_tests.values.map(&:size).reduce(:+)))
|
98
|
+
end
|
99
|
+
|
100
|
+
def exception_details spec, test, exception
|
101
|
+
puts(info([spec, test]*' / '))
|
102
|
+
puts(error(exception.message), indent: 2)
|
103
|
+
exception.backtrace.each {|l| puts(info(MiniSpec::Utils.shorten_source(l)), indent: 2)}
|
104
|
+
puts('---', '')
|
105
|
+
end
|
106
|
+
|
107
|
+
def failure_details spec, test, failure
|
108
|
+
puts(info([spec, test]*' / '))
|
109
|
+
puts(error(callerline(failure[:callers][0])), indent: 2)
|
110
|
+
callers(failure[:callers]).each {|l| puts(info(l), indent: 2)}
|
111
|
+
puts
|
112
|
+
return puts(*failure[:message].split("\n"), '', indent: 2) if failure[:message]
|
113
|
+
return if failure[:right_object] == :__ms__right_object
|
114
|
+
|
115
|
+
expected, actual = [:right_object, :left_object].map do |obj|
|
116
|
+
str = stringify_object(failure[obj])
|
117
|
+
[str =~ /\n/ ? :puts : :print, str]
|
118
|
+
end
|
119
|
+
|
120
|
+
send(expected.first, info(' Expected: '))
|
121
|
+
print('NOT ') if failure[:negation]
|
122
|
+
puts(expected.last)
|
123
|
+
|
124
|
+
send(actual.first, info(' Actual: '))
|
125
|
+
puts(actual.last)
|
126
|
+
|
127
|
+
print(info(' Compared using: '))
|
128
|
+
puts(failure[:right_method])
|
129
|
+
|
130
|
+
diff = diff(actual.last, expected.last)
|
131
|
+
puts(info(' Diff: '), diff) unless diff.empty?
|
132
|
+
puts('---', '')
|
133
|
+
end
|
134
|
+
|
135
|
+
def mark_as_passed spec, test
|
136
|
+
puts(success("OK"))
|
137
|
+
end
|
138
|
+
|
139
|
+
def mark_as_skipped spec, test, source_location
|
140
|
+
puts(warn("Skipped"))
|
141
|
+
(@skipped_tests[spec] ||= []).push([test, source_location])
|
142
|
+
end
|
143
|
+
|
144
|
+
def mark_as_failed spec, test, verb, proc, failures
|
145
|
+
puts(error("FAILED"))
|
146
|
+
(@failed_tests[spec] ||= []).push([test, verb, proc, failures])
|
147
|
+
end
|
148
|
+
|
149
|
+
def print(*args); @stdout.print(*indent_lines(*args)) end
|
150
|
+
def puts(*args); @stdout.puts(*indent_lines(*args)) end
|
151
|
+
|
152
|
+
{success: 32, info: 34, warn: 35, error: 31}.each_pair do |m,c|
|
153
|
+
define_method(m) {|s| "\e[%im%s\e[0m" % [c, s]}
|
154
|
+
end
|
155
|
+
|
156
|
+
def failures?
|
157
|
+
@failed_specs.any? || @failed_tests.any?
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
def indent_lines *args
|
162
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
163
|
+
(i = opts[:indent]) && (i = @@indent*i) && args.map {|l| i + l} || args
|
164
|
+
end
|
165
|
+
|
166
|
+
def callers *callers
|
167
|
+
callers.flatten.uniq.reverse.map {|l| MiniSpec::Utils.shorten_source(l)}
|
168
|
+
end
|
169
|
+
|
170
|
+
def callerline caller
|
171
|
+
file, line = caller.match(/^(.+?):(\d+)(?::in `(.*)')?/) {|m| m[1..2]}
|
172
|
+
return unless lines = MiniSpec.source_location_cache(file)
|
173
|
+
(line = lines[line.to_i - 1]) && line.strip
|
174
|
+
end
|
175
|
+
|
176
|
+
def stringify_object obj
|
177
|
+
obj.is_a?(String) ? obj : MiniSpec::Utils.pp(obj)
|
178
|
+
end
|
179
|
+
|
180
|
+
def diff actual, expected
|
181
|
+
@differ ||= MiniSpec::Differ.new(color: true)
|
182
|
+
@differ.diff(actual, expected).strip
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module MiniSpec
|
2
|
+
module Utils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def undefine_method object, method
|
6
|
+
return unless method_defined?(object, method)
|
7
|
+
object.instance_eval('undef :%s' % method)
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_defined? object, method
|
11
|
+
object.respond_to?(method) ||
|
12
|
+
object.protected_methods.include?(method) ||
|
13
|
+
object.private_methods.include?(method)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
# checking whether correct arguments passed to proxy methods.
|
18
|
+
#
|
19
|
+
# @raise [ArgumentError] if more than two arguments given
|
20
|
+
# @raise [ArgumentError] if both argument and block given
|
21
|
+
#
|
22
|
+
def valid_proxy_arguments? left_method, *args, &proc
|
23
|
+
args.size > 2 && raise(ArgumentError, '#%s - wrong number of arguments, %i for 0..2' % [left_method, args.size])
|
24
|
+
args.size > 0 && proc && raise(ArgumentError, '#%s accepts either arguments or a block, not both' % left_method)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @example
|
28
|
+
# received = [:a, :b, :c]
|
29
|
+
# expected = [1]
|
30
|
+
# => {:a=>1, :b=>1, :c=>1}
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# received = [:a, :b, :c]
|
34
|
+
# expected = []
|
35
|
+
# => {:a=>nil, :b=>nil, :c=>nil}
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# received = [:a, :b, :c]
|
39
|
+
# expected = [1, 2]
|
40
|
+
# => {:a=>1, :b=>2, :c=>2}
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# received = [:a, :b, :c]
|
44
|
+
# expected = [1, 2, 3, 4]
|
45
|
+
# => {:a=>1, :b=>2, :c=>3}
|
46
|
+
#
|
47
|
+
# @param received [Array]
|
48
|
+
# @param expected [Array]
|
49
|
+
# @return [Hash]
|
50
|
+
#
|
51
|
+
def zipper received, expected
|
52
|
+
result = {}
|
53
|
+
received.uniq.each_with_index do |m,i|
|
54
|
+
result[m] = expected[i] || expected[i-1] || expected[0]
|
55
|
+
end
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
# determines method's visibility
|
60
|
+
#
|
61
|
+
# @param object
|
62
|
+
# @param method
|
63
|
+
# @return [Symbol] or nil
|
64
|
+
#
|
65
|
+
def method_visibility object, method
|
66
|
+
{
|
67
|
+
public: :public_methods,
|
68
|
+
protected: :protected_methods,
|
69
|
+
private: :private_methods
|
70
|
+
}.each_pair do |v,m|
|
71
|
+
return v if object.send(m).include?(method)
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def array_elements_map array
|
77
|
+
# borrowed from thoughtbot/shoulda
|
78
|
+
array.inject({}) {|h,e| h[e] ||= array.select { |i| i == e }.size; h}
|
79
|
+
end
|
80
|
+
|
81
|
+
def source proc
|
82
|
+
shorten_source(proc.source_location*':')
|
83
|
+
end
|
84
|
+
|
85
|
+
# get rid of Dir.pwd from given path
|
86
|
+
def shorten_source source
|
87
|
+
source.to_s.sub(/\A#{Dir.pwd}\/?/, '')
|
88
|
+
end
|
89
|
+
|
90
|
+
# checks whether given label matches any matcher.
|
91
|
+
# even if label matched, it will return `false` if label matches some rejector.
|
92
|
+
#
|
93
|
+
# @param label
|
94
|
+
# @param matchers an `Array` of matchers and rejectors.
|
95
|
+
# matchers contained as hashes, rejectors as arrays.
|
96
|
+
# @return `true` or `false`
|
97
|
+
#
|
98
|
+
def any_match? label, matchers
|
99
|
+
reject, select = matchers.partition {|m| m.is_a?(Hash)}
|
100
|
+
|
101
|
+
rejected = rejected?(label, reject)
|
102
|
+
if select.any?
|
103
|
+
return select.find {|x| (x == :*) || match?(label, x)} && !rejected
|
104
|
+
end
|
105
|
+
!rejected
|
106
|
+
end
|
107
|
+
|
108
|
+
# checks whether given label matches any rejector.
|
109
|
+
#
|
110
|
+
# @param label
|
111
|
+
# @param reject an `Array` of rejectors, each being a `Hash` containing `:except` key
|
112
|
+
# @return `true` or `false`
|
113
|
+
#
|
114
|
+
def rejected? label, reject
|
115
|
+
if reject.any? && (x = reject.first[:except])
|
116
|
+
if x.is_a?(Array)
|
117
|
+
return true if x.find {|m| match?(label, m)}
|
118
|
+
else
|
119
|
+
return true if match?(label, x)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
false
|
123
|
+
end
|
124
|
+
|
125
|
+
# compare given label to given expression.
|
126
|
+
# if expression is a `Regexp` comparing using `=~`.
|
127
|
+
# otherwise `==` are used
|
128
|
+
#
|
129
|
+
# @param label
|
130
|
+
# @param x
|
131
|
+
# @return `true` or `false`
|
132
|
+
#
|
133
|
+
def match? label, x
|
134
|
+
x.is_a?(Regexp) ? label.to_s =~ x : label == x
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
Dir[File.expand_path('../utils/**/*.rb', __FILE__)].each {|f| require(f)}
|
@@ -0,0 +1,325 @@
|
|
1
|
+
# borrowed from RSpec - https://github.com/rspec/rspec-support
|
2
|
+
|
3
|
+
# Copyright (c) 2013 David Chelimsky, Myron Marston, Jon Rowe, Sam Phippen, Xavier Shay, Bradley Schaefer
|
4
|
+
#
|
5
|
+
# MIT License
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
module MiniSpec
|
27
|
+
class Differ
|
28
|
+
class EncodedString
|
29
|
+
|
30
|
+
MRI_UNICODE_UNKNOWN_CHARACTER = "\xEF\xBF\xBD".freeze
|
31
|
+
|
32
|
+
def initialize(string, encoding = nil)
|
33
|
+
@encoding = encoding
|
34
|
+
@source_encoding = detect_source_encoding(string)
|
35
|
+
@string = matching_encoding(string)
|
36
|
+
end
|
37
|
+
attr_reader :source_encoding
|
38
|
+
|
39
|
+
delegated_methods = String.instance_methods.map(&:to_s) & %w[eql? lines == encoding empty?]
|
40
|
+
delegated_methods.each do |name|
|
41
|
+
define_method(name) { |*args, &block| @string.__send__(name, *args, &block) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def <<(string)
|
45
|
+
@string << matching_encoding(string)
|
46
|
+
end
|
47
|
+
|
48
|
+
def split(regex_or_string)
|
49
|
+
@string.split(matching_encoding(regex_or_string))
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
@string
|
54
|
+
end
|
55
|
+
alias :to_str :to_s
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
if String.method_defined?(:encoding)
|
60
|
+
def matching_encoding(string)
|
61
|
+
string.encode(@encoding)
|
62
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
63
|
+
normalize_missing(string.encode(@encoding, :invalid => :replace, :undef => :replace))
|
64
|
+
rescue Encoding::ConverterNotFoundError
|
65
|
+
normalize_missing(string.force_encoding(@encoding).encode(:invalid => :replace))
|
66
|
+
end
|
67
|
+
|
68
|
+
def normalize_missing(string)
|
69
|
+
if @encoding.to_s == "UTF-8"
|
70
|
+
string.gsub(MRI_UNICODE_UNKNOWN_CHARACTER.force_encoding(@encoding), "?")
|
71
|
+
else
|
72
|
+
string
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def detect_source_encoding(string)
|
77
|
+
string.encoding
|
78
|
+
end
|
79
|
+
else
|
80
|
+
def matching_encoding(string)
|
81
|
+
string
|
82
|
+
end
|
83
|
+
|
84
|
+
def detect_source_encoding(string)
|
85
|
+
'US-ASCII'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class HunkGenerator
|
91
|
+
def initialize(actual, expected)
|
92
|
+
@actual = actual
|
93
|
+
@expected = expected
|
94
|
+
end
|
95
|
+
|
96
|
+
def hunks
|
97
|
+
@file_length_difference = 0
|
98
|
+
@hunks ||= diffs.map do |piece|
|
99
|
+
build_hunk(piece)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def diffs
|
106
|
+
Diff::LCS.diff(expected_lines, actual_lines)
|
107
|
+
end
|
108
|
+
|
109
|
+
def expected_lines
|
110
|
+
@expected.split("\n").map! { |e| e.chomp }
|
111
|
+
end
|
112
|
+
|
113
|
+
def actual_lines
|
114
|
+
@actual.split("\n").map! { |e| e.chomp }
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_hunk(piece)
|
118
|
+
Diff::LCS::Hunk.new(
|
119
|
+
expected_lines, actual_lines, piece, context_lines, @file_length_difference
|
120
|
+
).tap do |h|
|
121
|
+
@file_length_difference = h.file_length_difference
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def context_lines
|
126
|
+
3
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def diff(actual, expected)
|
131
|
+
diff = ""
|
132
|
+
|
133
|
+
if actual && expected
|
134
|
+
if all_strings?(actual, expected)
|
135
|
+
if any_multiline_strings?(actual, expected)
|
136
|
+
diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected))
|
137
|
+
end
|
138
|
+
elsif no_procs?(actual, expected) && no_numbers?(actual, expected)
|
139
|
+
diff = diff_as_object(actual, expected)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
diff.to_s
|
144
|
+
end
|
145
|
+
|
146
|
+
def diff_as_string(actual, expected)
|
147
|
+
@encoding = pick_encoding actual, expected
|
148
|
+
|
149
|
+
@actual = EncodedString.new(actual, @encoding)
|
150
|
+
@expected = EncodedString.new(expected, @encoding)
|
151
|
+
|
152
|
+
output = EncodedString.new("\n", @encoding)
|
153
|
+
|
154
|
+
hunks.each_cons(2) do |prev_hunk, current_hunk|
|
155
|
+
begin
|
156
|
+
if current_hunk.overlaps?(prev_hunk)
|
157
|
+
add_old_hunk_to_hunk(current_hunk, prev_hunk)
|
158
|
+
else
|
159
|
+
add_to_output(output, prev_hunk.diff(format).to_s)
|
160
|
+
end
|
161
|
+
ensure
|
162
|
+
add_to_output(output, "\n")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
if hunks.last
|
167
|
+
finalize_output(output, hunks.last.diff(format).to_s)
|
168
|
+
end
|
169
|
+
|
170
|
+
color_diff output
|
171
|
+
rescue Encoding::CompatibilityError
|
172
|
+
handle_encoding_errors
|
173
|
+
end
|
174
|
+
|
175
|
+
def diff_as_object(actual, expected)
|
176
|
+
actual_as_string = object_to_string(actual)
|
177
|
+
expected_as_string = object_to_string(expected)
|
178
|
+
diff_as_string(actual_as_string, expected_as_string)
|
179
|
+
end
|
180
|
+
|
181
|
+
attr_reader :color
|
182
|
+
alias_method :color?, :color
|
183
|
+
|
184
|
+
def initialize(opts={})
|
185
|
+
@color = opts.fetch(:color, false)
|
186
|
+
@object_preparer = opts.fetch(:object_preparer, lambda { |string| string })
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def no_procs?(*args)
|
192
|
+
args.flatten.none? { |a| Proc === a}
|
193
|
+
end
|
194
|
+
|
195
|
+
def all_strings?(*args)
|
196
|
+
args.flatten.all? { |a| String === a}
|
197
|
+
end
|
198
|
+
|
199
|
+
def any_multiline_strings?(*args)
|
200
|
+
all_strings?(*args) && args.flatten.any? { |a| multiline?(a) }
|
201
|
+
end
|
202
|
+
|
203
|
+
def no_numbers?(*args)
|
204
|
+
args.flatten.none? { |a| Numeric === a}
|
205
|
+
end
|
206
|
+
|
207
|
+
def coerce_to_string(string_or_array)
|
208
|
+
return string_or_array unless Array === string_or_array
|
209
|
+
diffably_stringify(string_or_array).join("\n")
|
210
|
+
end
|
211
|
+
|
212
|
+
def diffably_stringify(array)
|
213
|
+
array.map do |entry|
|
214
|
+
if Array === entry
|
215
|
+
entry.inspect
|
216
|
+
else
|
217
|
+
entry.to_s.gsub("\n", "\\n")
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
if String.method_defined?(:encoding)
|
223
|
+
def multiline?(string)
|
224
|
+
string.include?("\n".encode(string.encoding))
|
225
|
+
end
|
226
|
+
else
|
227
|
+
def multiline?(string)
|
228
|
+
string.include?("\n")
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def hunks
|
233
|
+
@hunks ||= HunkGenerator.new(@actual, @expected).hunks
|
234
|
+
end
|
235
|
+
|
236
|
+
def finalize_output(output, final_line)
|
237
|
+
add_to_output(output, final_line)
|
238
|
+
add_to_output(output, "\n")
|
239
|
+
end
|
240
|
+
|
241
|
+
def add_to_output(output, string)
|
242
|
+
output << string
|
243
|
+
end
|
244
|
+
|
245
|
+
def add_old_hunk_to_hunk(hunk, oldhunk)
|
246
|
+
hunk.merge(oldhunk)
|
247
|
+
end
|
248
|
+
|
249
|
+
def format
|
250
|
+
:unified
|
251
|
+
end
|
252
|
+
|
253
|
+
def color(text, color_code)
|
254
|
+
"\e[#{color_code}m#{text}\e[0m"
|
255
|
+
end
|
256
|
+
|
257
|
+
def red(text)
|
258
|
+
color(text, 31)
|
259
|
+
end
|
260
|
+
|
261
|
+
def green(text)
|
262
|
+
color(text, 32)
|
263
|
+
end
|
264
|
+
|
265
|
+
def blue(text)
|
266
|
+
color(text, 34)
|
267
|
+
end
|
268
|
+
|
269
|
+
def normal(text)
|
270
|
+
color(text, 0)
|
271
|
+
end
|
272
|
+
|
273
|
+
def color_diff(diff)
|
274
|
+
return diff unless color?
|
275
|
+
|
276
|
+
diff.lines.map { |line|
|
277
|
+
case line[0].chr
|
278
|
+
when "+"
|
279
|
+
green line
|
280
|
+
when "-"
|
281
|
+
red line
|
282
|
+
when "@"
|
283
|
+
line[1].chr == "@" ? blue(line) : normal(line)
|
284
|
+
else
|
285
|
+
normal(line)
|
286
|
+
end
|
287
|
+
}.join
|
288
|
+
end
|
289
|
+
|
290
|
+
def object_to_string(object)
|
291
|
+
object = @object_preparer.call(object)
|
292
|
+
case object
|
293
|
+
when Hash
|
294
|
+
object.keys.sort_by { |k| k.to_s }.map do |key|
|
295
|
+
pp_key = PP.singleline_pp(key, "")
|
296
|
+
pp_value = PP.singleline_pp(object[key], "")
|
297
|
+
|
298
|
+
"#{pp_key} => #{pp_value},"
|
299
|
+
end.join("\n")
|
300
|
+
when String
|
301
|
+
object =~ /\n/ ? object : object.inspect
|
302
|
+
else
|
303
|
+
PP.pp(object,"")
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
if String.method_defined?(:encoding)
|
308
|
+
def pick_encoding(source_a, source_b)
|
309
|
+
Encoding.compatible?(source_a, source_b) || Encoding.default_external
|
310
|
+
end
|
311
|
+
else
|
312
|
+
def pick_encoding(source_a, source_b)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def handle_encoding_errors
|
317
|
+
if @actual.source_encoding != @expected.source_encoding
|
318
|
+
"Could not produce a diff because the encoding of the actual string (#{@actual.source_encoding}) "+
|
319
|
+
"differs from the encoding of the expected string (#{@expected.source_encoding})"
|
320
|
+
else
|
321
|
+
"Could not produce a diff because of the encoding of the string (#{@expected.source_encoding})"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|