minispec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.pryrc +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +2140 -0
  6. data/Rakefile +11 -0
  7. data/bin/minispec +4 -0
  8. data/lib/minispec.rb +175 -0
  9. data/lib/minispec/api.rb +2 -0
  10. data/lib/minispec/api/class.rb +195 -0
  11. data/lib/minispec/api/class/after.rb +49 -0
  12. data/lib/minispec/api/class/around.rb +54 -0
  13. data/lib/minispec/api/class/before.rb +101 -0
  14. data/lib/minispec/api/class/helpers.rb +116 -0
  15. data/lib/minispec/api/class/let.rb +44 -0
  16. data/lib/minispec/api/class/tests.rb +33 -0
  17. data/lib/minispec/api/instance.rb +158 -0
  18. data/lib/minispec/api/instance/mocks/doubles.rb +36 -0
  19. data/lib/minispec/api/instance/mocks/mocks.rb +319 -0
  20. data/lib/minispec/api/instance/mocks/spies.rb +17 -0
  21. data/lib/minispec/api/instance/mocks/stubs.rb +105 -0
  22. data/lib/minispec/helpers.rb +1 -0
  23. data/lib/minispec/helpers/array.rb +56 -0
  24. data/lib/minispec/helpers/booleans.rb +108 -0
  25. data/lib/minispec/helpers/generic.rb +24 -0
  26. data/lib/minispec/helpers/mocks/expectations.rb +29 -0
  27. data/lib/minispec/helpers/mocks/spies.rb +36 -0
  28. data/lib/minispec/helpers/raise.rb +44 -0
  29. data/lib/minispec/helpers/throw.rb +29 -0
  30. data/lib/minispec/mocks.rb +11 -0
  31. data/lib/minispec/mocks/expectations.rb +77 -0
  32. data/lib/minispec/mocks/stubs.rb +178 -0
  33. data/lib/minispec/mocks/validations.rb +80 -0
  34. data/lib/minispec/mocks/validations/amount.rb +63 -0
  35. data/lib/minispec/mocks/validations/arguments.rb +161 -0
  36. data/lib/minispec/mocks/validations/caller.rb +43 -0
  37. data/lib/minispec/mocks/validations/order.rb +47 -0
  38. data/lib/minispec/mocks/validations/raise.rb +111 -0
  39. data/lib/minispec/mocks/validations/return.rb +74 -0
  40. data/lib/minispec/mocks/validations/throw.rb +91 -0
  41. data/lib/minispec/mocks/validations/yield.rb +141 -0
  42. data/lib/minispec/proxy.rb +201 -0
  43. data/lib/minispec/reporter.rb +185 -0
  44. data/lib/minispec/utils.rb +139 -0
  45. data/lib/minispec/utils/differ.rb +325 -0
  46. data/lib/minispec/utils/pretty_print.rb +51 -0
  47. data/lib/minispec/utils/raise.rb +123 -0
  48. data/lib/minispec/utils/throw.rb +140 -0
  49. data/minispec.gemspec +27 -0
  50. data/test/mocks/expectations/amount.rb +67 -0
  51. data/test/mocks/expectations/arguments.rb +126 -0
  52. data/test/mocks/expectations/caller.rb +55 -0
  53. data/test/mocks/expectations/generic.rb +35 -0
  54. data/test/mocks/expectations/order.rb +46 -0
  55. data/test/mocks/expectations/raise.rb +166 -0
  56. data/test/mocks/expectations/return.rb +71 -0
  57. data/test/mocks/expectations/throw.rb +113 -0
  58. data/test/mocks/expectations/yield.rb +109 -0
  59. data/test/mocks/spies/amount.rb +68 -0
  60. data/test/mocks/spies/arguments.rb +57 -0
  61. data/test/mocks/spies/generic.rb +61 -0
  62. data/test/mocks/spies/order.rb +38 -0
  63. data/test/mocks/spies/raise.rb +158 -0
  64. data/test/mocks/spies/return.rb +71 -0
  65. data/test/mocks/spies/throw.rb +113 -0
  66. data/test/mocks/spies/yield.rb +101 -0
  67. data/test/mocks/test__doubles.rb +98 -0
  68. data/test/mocks/test__expectations.rb +27 -0
  69. data/test/mocks/test__mocks.rb +197 -0
  70. data/test/mocks/test__proxies.rb +61 -0
  71. data/test/mocks/test__spies.rb +43 -0
  72. data/test/mocks/test__stubs.rb +427 -0
  73. data/test/proxified_asserts.rb +34 -0
  74. data/test/setup.rb +53 -0
  75. data/test/test__around.rb +58 -0
  76. data/test/test__assert.rb +510 -0
  77. data/test/test__before_and_after.rb +117 -0
  78. data/test/test__before_and_after_all.rb +71 -0
  79. data/test/test__helpers.rb +197 -0
  80. data/test/test__raise.rb +104 -0
  81. data/test/test__skip.rb +41 -0
  82. data/test/test__throw.rb +103 -0
  83. 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