minispec 0.0.1
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.
- 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
|