# frozen_string_literal: true RSpec::Support.require_rspec_support 'encoded_string' RSpec::Support.require_rspec_support 'hunk_generator' RSpec::Support.require_rspec_support "object_formatter" require 'pp' module RSpec module Support # rubocop:disable Metrics/ClassLength class Differ def diff(actual, expected) diff = "" unless actual.nil? || expected.nil? if all_strings?(actual, expected) if any_multiline_strings?(actual, expected) diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected)) end elsif no_procs?(actual, expected) && no_numbers?(actual, expected) diff = diff_as_object(actual, expected) end end diff.to_s end # rubocop:disable Metrics/MethodLength def diff_as_string(actual, expected) encoding = EncodedString.pick_encoding(actual, expected) actual = EncodedString.new(actual, encoding) expected = EncodedString.new(expected, encoding) output = EncodedString.new("\n", encoding) hunks = build_hunks(actual, expected) hunks.each_cons(2) do |prev_hunk, current_hunk| begin if current_hunk.overlaps?(prev_hunk) add_old_hunk_to_hunk(current_hunk, prev_hunk) else add_to_output(output, prev_hunk.diff(format_type).to_s) end ensure add_to_output(output, "\n") end end finalize_output(output, hunks.last.diff(format_type).to_s) if hunks.last color_diff output rescue Encoding::CompatibilityError handle_encoding_errors(actual, expected) end # rubocop:enable Metrics/MethodLength def diff_as_object(actual, expected) actual_as_string = object_to_string(actual) expected_as_string = object_to_string(expected) diff_as_string(actual_as_string, expected_as_string) end def color? @color end def initialize(opts={}) @color = opts.fetch(:color, false) @object_preparer = opts.fetch(:object_preparer, lambda { |string| string }) end private def no_procs?(*args) safely_flatten(args).none? { |a| Proc === a } end def all_strings?(*args) safely_flatten(args).all? { |a| String === a } end def any_multiline_strings?(*args) all_strings?(*args) && safely_flatten(args).any? { |a| multiline?(a) } end def no_numbers?(*args) safely_flatten(args).none? { |a| Numeric === a } end def coerce_to_string(string_or_array) return string_or_array unless Array === string_or_array diffably_stringify(string_or_array).join("\n") end def diffably_stringify(array) array.map do |entry| if Array === entry entry.inspect else entry.to_s.gsub("\n", "\\n").gsub("\r", "\\r") end end end if String.method_defined?(:encoding) def multiline?(string) string.include?("\n".encode(string.encoding)) end else def multiline?(string) string.include?("\n") end end def build_hunks(actual, expected) HunkGenerator.new(actual, expected).hunks end def finalize_output(output, final_line) add_to_output(output, final_line) add_to_output(output, "\n") end def add_to_output(output, string) output << string end def add_old_hunk_to_hunk(hunk, oldhunk) hunk.merge(oldhunk) end def safely_flatten(array) array = array.flatten(1) until (array == array.flatten(1)) array end def format_type :unified end def color(text, color_code) "\e[#{color_code}m#{text}\e[0m" end def red(text) color(text, 31) end def green(text) color(text, 32) end def blue(text) color(text, 34) end def normal(text) color(text, 0) end def color_diff(diff) return diff unless color? diff.lines.map do |line| case line[0].chr when "+" green line when "-" red line when "@" line[1].chr == "@" ? blue(line) : normal(line) else normal(line) end end.join end def object_to_string(object) object = @object_preparer.call(object) case object when Hash hash_to_string(object) when Array PP.pp(ObjectFormatter.prepare_for_inspection(object), "".dup) when String object =~ /\n/ ? object : object.inspect else PP.pp(object, "".dup) end end def hash_to_string(hash) formatted_hash = ObjectFormatter.prepare_for_inspection(hash) formatted_hash.keys.sort_by { |k| k.to_s }.map do |key| pp_key = PP.singleline_pp(key, "".dup) pp_value = PP.singleline_pp(formatted_hash[key], "".dup) "#{pp_key} => #{pp_value}," end.join("\n") end def handle_encoding_errors(actual, expected) if actual.source_encoding != expected.source_encoding "Could not produce a diff because the encoding of the actual string " \ "(#{actual.source_encoding}) differs from the encoding of the expected " \ "string (#{expected.source_encoding})" else "Could not produce a diff because of the encoding of the string " \ "(#{expected.source_encoding})" end end end # rubocop:enable Metrics/ClassLength end end