diff --git a/.rubocop.yml b/.rubocop.yml index e65e0e10..505fc3da 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -50,6 +50,7 @@ Metrics/BlockLength: - shared_context - shared_examples - shared_examples_for + - xit Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: @@ -62,6 +63,7 @@ Metrics/LineLength: - "^\\s*context" - "^\\s*describe" - "^\\s*it" + - "^\\s*xit" Metrics/MethodLength: Enabled: false Metrics/ModuleLength: @@ -85,6 +87,10 @@ Style/AccessModifierDeclarations: Enabled: false Style/Alias: Enabled: false +Style/AndOr: + Enabled: false +Style/BlockComments: + Enabled: false Style/CollectionMethods: Enabled: true Style/Documentation: @@ -113,12 +119,16 @@ Style/MultilineBlockChain: Enabled: false Style/NegatedIf: Enabled: false +Style/Next: + Enabled: false Style/NegatedWhile: Enabled: false Style/NumericPredicate: Enabled: false Style/OneLineConditional: Enabled: false +Style/ParenthesesAroundCondition: + Enabled: false Style/PercentLiteralDelimiters: Enabled: false Style/PreferredHashMethods: diff --git a/lib/super_diff.rb b/lib/super_diff.rb index 3473cfa6..70dd9825 100644 --- a/lib/super_diff.rb +++ b/lib/super_diff.rb @@ -10,6 +10,7 @@ module SuperDiff autoload :Csi, "super_diff/csi" autoload :DiffFormatter, "super_diff/diff_formatter" autoload :DiffFormatters, "super_diff/diff_formatters" + autoload :DiffLegendBuilder, "super_diff/diff_legend_builder" autoload :Differ, "super_diff/differ" autoload :Differs, "super_diff/differs" autoload :EqualityMatcher, "super_diff/equality_matcher" @@ -26,11 +27,6 @@ module SuperDiff autoload :OperationSequences, "super_diff/operation_sequences" autoload :Operations, "super_diff/operations" autoload :RecursionGuard, "super_diff/recursion_guard" - - COLORS = { - alpha: :magenta, - beta: :yellow, - border: :blue, - header: :white, - }.freeze end + +require "super_diff/colors" diff --git a/lib/super_diff/colorized_document_extensions.rb b/lib/super_diff/colorized_document_extensions.rb index e0ca5ce5..d9a482e4 100644 --- a/lib/super_diff/colorized_document_extensions.rb +++ b/lib/super_diff/colorized_document_extensions.rb @@ -13,5 +13,17 @@ def alpha(*args, **opts, &block) def beta(*args, **opts, &block) colorize(*args, **opts, fg: SuperDiff::COLORS.fetch(:beta), &block) end + + def highlight(*args, **opts, &block) + colorize(*args, **opts, fg: SuperDiff::COLORS.fetch(:highlight), &block) + end + + def header(*args, **opts, &block) + colorize(*args, **opts, fg: SuperDiff::COLORS.fetch(:header), &block) + end + + def border(*args, **opts, &block) + colorize(*args, **opts, fg: SuperDiff::COLORS.fetch(:border), &block) + end end end diff --git a/lib/super_diff/colors.rb b/lib/super_diff/colors.rb new file mode 100644 index 00000000..d6e4e1d5 --- /dev/null +++ b/lib/super_diff/colors.rb @@ -0,0 +1,9 @@ +module SuperDiff + COLORS = { + alpha: :magenta, + beta: :yellow, + border: :blue, + highlight: :blue, + header: :white, + }.freeze +end diff --git a/lib/super_diff/csi.rb b/lib/super_diff/csi.rb index 0cd845ac..4232be06 100644 --- a/lib/super_diff/csi.rb +++ b/lib/super_diff/csi.rb @@ -11,6 +11,7 @@ module Csi autoload :ResetSequence, "super_diff/csi/reset_sequence" autoload :TwentyFourBitColor, "super_diff/csi/twenty_four_bit_color" autoload :UncolorizedDocument, "super_diff/csi/uncolorized_document" + autoload :UnderlineSequence, "super_diff/csi/underline_sequence" class << self attr_writer :color_enabled diff --git a/lib/super_diff/csi/colorized_document.rb b/lib/super_diff/csi/colorized_document.rb index 1256c7e4..52c482f6 100644 --- a/lib/super_diff/csi/colorized_document.rb +++ b/lib/super_diff/csi/colorized_document.rb @@ -17,7 +17,7 @@ def colorize_block(colors, opts, &block) add_part(color_sequence) color_sequences_open_in_parent << color_sequence - evaluate_block(&block) + apply(&block) add_part(Csi.reset_sequence) color_sequence_to_reopen = color_sequences_open_in_parent.pop diff --git a/lib/super_diff/csi/document.rb b/lib/super_diff/csi/document.rb index c4a0dcde..91a021d8 100644 --- a/lib/super_diff/csi/document.rb +++ b/lib/super_diff/csi/document.rb @@ -8,10 +8,14 @@ def initialize(&block) @indentation_stack = [] if block - evaluate_block(&block) + apply(&block) end end + def +(other) + to_s + other.to_s + end + def each(&block) parts.each(&block) end @@ -20,6 +24,10 @@ def bold(*args, **opts, &block) colorize(BoldSequence.new, *args, **opts, &block) end + def underline(*args, **opts, &block) + colorize(UnderlineSequence.new, *args, **opts, &block) + end + def colorize(*args, **opts, &block) contents, colors = args.partition do |arg| arg.is_a?(String) || arg.is_a?(self.class) @@ -45,7 +53,7 @@ def colorize(*args, **opts, &block) def text(*contents, **, &block) if block - evaluate_block(&block) + apply(&block) elsif contents.any? contents.each do |part| add_part(part) @@ -60,10 +68,14 @@ def text(*contents, **, &block) def line(*contents, indent_by: 0, &block) indent(by: indent_by) do + # if block + # binding.pry + # end + add_part(indentation_stack.join) if block - evaluate_block(&block) + apply(&block) elsif contents.any? text(*contents) else @@ -75,6 +87,7 @@ def line(*contents, indent_by: 0, &block) add_part("\n") end + alias_method :add_line, :line def newline add_part("\n") @@ -83,10 +96,18 @@ def newline def indent(by:, &block) # TODO: This won't work if using `text` manually to add lines indentation_stack << (by.is_a?(String) ? by : " " * by) - evaluate_block(&block) + apply(&block) indentation_stack.pop end + def apply(&block) + if block.arity > 0 + block.call(self) + else + instance_eval(&block) + end + end + def method_missing(name, *args, **opts, &block) request = derive_request_from(name) @@ -128,14 +149,6 @@ def derive_request_from(name) end end - def evaluate_block(&block) - if block.arity > 0 - block.call(self) - else - instance_eval(&block) - end - end - def add_part(part) parts.push(part) end @@ -165,16 +178,16 @@ def wrapper class MethodRequest < Request def resolve(doc, args, opts, &block) - doc.public_send(wrapper) do |d| - d.public_send(name, *args, **opts, &block) + doc.public_send(wrapper, **opts) do |d| + d.public_send(name, *args, &block) end end end class ColorRequest < Request def resolve(doc, args, opts, &block) - doc.public_send(wrapper) do |d| - d.colorize(*args, **opts, fg: name, &block) + doc.public_send(wrapper, **opts) do |d| + d.colorize(*args, fg: name, &block) end end end diff --git a/lib/super_diff/csi/uncolorized_document.rb b/lib/super_diff/csi/uncolorized_document.rb index 3760069a..3d233018 100644 --- a/lib/super_diff/csi/uncolorized_document.rb +++ b/lib/super_diff/csi/uncolorized_document.rb @@ -4,7 +4,7 @@ class UncolorizedDocument < Document protected def colorize_block(*, &block) - evaluate_block(&block) + apply(&block) end def colorize_inline(contents, *) diff --git a/lib/super_diff/csi/underline_sequence.rb b/lib/super_diff/csi/underline_sequence.rb new file mode 100644 index 00000000..39b052b4 --- /dev/null +++ b/lib/super_diff/csi/underline_sequence.rb @@ -0,0 +1,9 @@ +module SuperDiff + module Csi + class UnderlineSequence + def to_s + "\e[4m" + end + end + end +end diff --git a/lib/super_diff/diff_legend_builder.rb b/lib/super_diff/diff_legend_builder.rb new file mode 100644 index 00000000..c7ca5c56 --- /dev/null +++ b/lib/super_diff/diff_legend_builder.rb @@ -0,0 +1,43 @@ +module SuperDiff + class DiffLegendBuilder + extend AttrExtras.mixin + + method_object :expected + + def call + SuperDiff::Helpers.style do + line do + header "Diff:" + end + + newline + + line do + border "┌ (Key) ──────────────────────────┐" + end + + line do + border "│ " + alpha "‹-› in expected, not in actual" + border " │" + end + + line do + border "│ " + beta "‹+› in actual, not in expected" + border " │" + end + + line do + border "│ " + plain "‹ › in both expected and actual" + border " │" + end + + line do + border "└─────────────────────────────────┘" + end + end + end + end +end diff --git a/lib/super_diff/helpers.rb b/lib/super_diff/helpers.rb index ef618adf..21bbad09 100644 --- a/lib/super_diff/helpers.rb +++ b/lib/super_diff/helpers.rb @@ -12,8 +12,8 @@ def self.style(*args, color_enabled: true, **opts, &block) document = klass.new.extend(ColorizedDocumentExtensions) if block - document.__send__(:evaluate_block, &block) - else + document.apply(&block) + elsif args.any? || opts.any? document.colorize(*args, **opts) end diff --git a/lib/super_diff/rspec.rb b/lib/super_diff/rspec.rb index 4c0cdd1d..2c46981b 100644 --- a/lib/super_diff/rspec.rb +++ b/lib/super_diff/rspec.rb @@ -6,10 +6,19 @@ module RSpec autoload :Configuration, "super_diff/rspec/configuration" autoload :Differ, "super_diff/rspec/differ" autoload :Differs, "super_diff/rspec/differs" + autoload( + :ExceptionMessageFormatters, + "super_diff/rspec/exception_message_formatters", + ) + autoload( + :ExceptionMessageFormatterMap, + "super_diff/rspec/exception_message_formatter_map", + ) autoload :MatcherTextBuilders, "super_diff/rspec/matcher_text_builders" autoload :MatcherTextTemplate, "super_diff/rspec/matcher_text_template" autoload :ObjectInspection, "super_diff/rspec/object_inspection" autoload :OperationalSequencers, "super_diff/rspec/operational_sequencers" + autoload :StringTagger, "super_diff/rspec/string_tagger" class << self attr_accessor :extra_differ_classes @@ -25,6 +34,16 @@ def self.configuration @_configuration ||= Configuration.new end + def self.format_exception_message(exception_message) + exception_message_formatter_map. + call(exception_message). + call(exception_message) + end + + def self.exception_message_formatter_map + @_exception_message_formatter_map ||= ExceptionMessageFormatterMap.new + end + def self.a_hash_including_something?(value) fuzzy_object?(value) && value.respond_to?(:expecteds) && diff --git a/lib/super_diff/rspec/differ.rb b/lib/super_diff/rspec/differ.rb index 9698ed70..f017c57c 100644 --- a/lib/super_diff/rspec/differ.rb +++ b/lib/super_diff/rspec/differ.rb @@ -6,6 +6,8 @@ class Differ static_facade :diff, :actual, :expected def diff + # binding.pry + if worth_diffing? diff = SuperDiff::Differ.call( expected, diff --git a/lib/super_diff/rspec/exception_message_formatter_map.rb b/lib/super_diff/rspec/exception_message_formatter_map.rb new file mode 100644 index 00000000..4d2eff4f --- /dev/null +++ b/lib/super_diff/rspec/exception_message_formatter_map.rb @@ -0,0 +1,23 @@ +module SuperDiff + module RSpec + class ExceptionMessageFormatterMap + class NotFoundError < StandardError; end + + def call(exception_message) + strategies = [ + ExceptionMessageFormatters::ExpectationError, + ExceptionMessageFormatters::UnexpectedMessageArgsError, + ExceptionMessageFormatters::Default, + ] + + found_strategy = strategies.find do |strategy| + strategy.applies_to?(exception_message) + end + + found_strategy or raise NotFoundError.new( + "Could not find an appropriate formatter class!", + ) + end + end + end +end diff --git a/lib/super_diff/rspec/exception_message_formatters.rb b/lib/super_diff/rspec/exception_message_formatters.rb new file mode 100644 index 00000000..34e25eae --- /dev/null +++ b/lib/super_diff/rspec/exception_message_formatters.rb @@ -0,0 +1,16 @@ +module SuperDiff + module RSpec + module ExceptionMessageFormatters + autoload :Base, "super_diff/rspec/exception_message_formatters/base" + autoload :Default, "super_diff/rspec/exception_message_formatters/default" + autoload( + :ExpectationError, + "super_diff/rspec/exception_message_formatters/expectation_error", + ) + autoload( + :UnexpectedMessageArgsError, + "super_diff/rspec/exception_message_formatters/unexpected_message_args_error", + ) + end + end +end diff --git a/lib/super_diff/rspec/exception_message_formatters/base.rb b/lib/super_diff/rspec/exception_message_formatters/base.rb new file mode 100644 index 00000000..b6c3f75c --- /dev/null +++ b/lib/super_diff/rspec/exception_message_formatters/base.rb @@ -0,0 +1,43 @@ +module SuperDiff + module RSpec + module ExceptionMessageFormatters + class Base + extend AttrExtras.mixin + + def self.applies_to?(message) + regex.match?(message.to_s) + end + + def self.regex + raise NotImplementedError.new( + "Please implement .regex in your subclass", + ) + end + + attr_private :message + + def self.call(message) + new(message).call + end + + def initialize(message) + @message = message.to_s + end + + def call + ranges.map(&:to_s).join + end + + protected + + def ranges + @_ranges ||= StringTagger.call( + string: message, + regex: self.class.regex, + colors: colors, + ) + end + end + end + end +end diff --git a/lib/super_diff/rspec/exception_message_formatters/default.rb b/lib/super_diff/rspec/exception_message_formatters/default.rb new file mode 100644 index 00000000..75e0c322 --- /dev/null +++ b/lib/super_diff/rspec/exception_message_formatters/default.rb @@ -0,0 +1,19 @@ +module SuperDiff + module RSpec + module ExceptionMessageFormatters + class Default < Base + extend AttrExtras.mixin + + method_object :message + + def self.applies_to?(_message) + true + end + + def call + message.to_s + end + end + end + end +end diff --git a/lib/super_diff/rspec/exception_message_formatters/expectation_error.rb b/lib/super_diff/rspec/exception_message_formatters/expectation_error.rb new file mode 100644 index 00000000..5af10795 --- /dev/null +++ b/lib/super_diff/rspec/exception_message_formatters/expectation_error.rb @@ -0,0 +1,17 @@ +module SuperDiff + module RSpec + module ExceptionMessageFormatters + class ExpectationError < Base + def self.regex + /\A\(.+\)\..+\(.+\)\s+expected: (\d+) times? with (any) arguments\s+received: (\d+) times with (any) arguments\Z/.freeze + end + + protected + + def colors + [:alpha, :alpha, :beta, :beta] + end + end + end + end +end diff --git a/lib/super_diff/rspec/exception_message_formatters/unexpected_message_args_error.rb b/lib/super_diff/rspec/exception_message_formatters/unexpected_message_args_error.rb new file mode 100644 index 00000000..5dba6e54 --- /dev/null +++ b/lib/super_diff/rspec/exception_message_formatters/unexpected_message_args_error.rb @@ -0,0 +1,26 @@ +module SuperDiff + module RSpec + module ExceptionMessageFormatters + class UnexpectedMessageArgsError < Base + def self.regex + /\A(.+) received (:\w+) with unexpected arguments\s+expected: (\(.+\))\s+got: (\(.+\))\Z/.freeze + end + + # colorize /.+/, :generic + # skip " received " + # colorize /:\w+/, :generic + # skip " with unexpected arguments\s" + # skip "expected: " + # colorize /\(.+\)/, :alpha + # skip "\s+got: " + # colorize /\(.+\)/, :beta + + protected + + def colors + [:generic, :generic, :alpha, :beta] + end + end + end + end +end diff --git a/lib/super_diff/rspec/monkey_patches.rb b/lib/super_diff/rspec/monkey_patches.rb index a718a057..3b93e974 100644 --- a/lib/super_diff/rspec/monkey_patches.rb +++ b/lib/super_diff/rspec/monkey_patches.rb @@ -9,6 +9,7 @@ require "rspec/matchers/built_in/have_attributes" require "rspec/matchers/built_in/include" require "rspec/matchers/built_in/match" +require "rspec/mocks/error_generator" module RSpec module Expectations @@ -225,6 +226,24 @@ def failure_slash_error_lines lines end +=begin + def exception_lines + lines = [] + lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ + # Run the exception message through the registered exception message + # formatters + message = SuperDiff::RSpec.format_exception_message( + encoded_string(exception.message.to_s) + ) + message.split("\n").each do |line| + # Don't double-indent lines that already have indentation + # lines << ((line.empty? || line.match?(/^[ ]+/)) ? line : " #{line}") + lines << (line.empty? ? line : " #{line}") + end + lines + end +=end + # Exclude this file from being included in backtraces, so that the # SnippetExtractor prints the right thing def find_failed_line @@ -268,39 +287,7 @@ class ExpectedsForMultipleDiffs # Add a key for different sides def self.from(expected) return expected if self === expected - - text = - colorizer.wrap("Diff:", SuperDiff::COLORS.fetch(:header)) + - "\n\n" + - colorizer.wrap( - "┌ (Key) ──────────────────────────┐", - SuperDiff::COLORS.fetch(:border) - ) + - "\n" + - colorizer.wrap("│ ", SuperDiff::COLORS.fetch(:border)) + - colorizer.wrap( - "‹-› in expected, not in actual", - SuperDiff::COLORS.fetch(:alpha) - ) + - colorizer.wrap(" │", SuperDiff::COLORS.fetch(:border)) + - "\n" + - colorizer.wrap("│ ", SuperDiff::COLORS.fetch(:border)) + - colorizer.wrap( - "‹+› in actual, not in expected", - SuperDiff::COLORS.fetch(:beta) - ) + - colorizer.wrap(" │", SuperDiff::COLORS.fetch(:border)) + - "\n" + - colorizer.wrap("│ ", SuperDiff::COLORS.fetch(:border)) + - "‹ › in both expected and actual" + - colorizer.wrap(" │", SuperDiff::COLORS.fetch(:border)) + - "\n" + - colorizer.wrap( - "└─────────────────────────────────┘", - SuperDiff::COLORS.fetch(:border) - ) - - new([[expected, text]]) + new([[expected, SuperDiff::DiffLegendBuilder.call(expected)]]) end def self.colorizer @@ -729,4 +716,237 @@ def match_array(items) end alias_matcher :an_array_matching, :match_array end + + module Mocks + class ErrorGenerator + def default_error_message(expectation, expected_args, actual_args) + SuperDiff::Helpers.style do |doc| + doc.red_line( + "#{intro} received ##{expectation.message} " + + "with unexpected arguments." + ) + + doc.newline + + doc.line "Expected: #{expected_args}" + doc.line " Got: #{actual_args}" + end.to_s + end + + def raise_expectation_error( + message, + expected_received_count, + argument_list_matcher, + actual_received_count, + expectation_count_type, + args, + backtrace_line = nil, + source_id = nil + ) + expected_part = expected_part_of_expectation_error( + expected_received_count, + expectation_count_type, + argument_list_matcher + ) + received_part = received_part_of_expectation_error( + actual_received_count, + args + ) + + error_message = [ + SuperDiff::Helpers.style( + :red, + "Expectation failed for double: " + + "#{@target.class}##{message}" + ), + "\n\n", + "#{expected_part}\n", + "#{received_part}" + ].join + + __raise(error_message, backtrace_line, source_id) + end + + def raise_unimplemented_error(doubled_module, method_name, object) + message = SuperDiff::Helpers.style do + line do + red "Could not place double." + end + + newline + end + + case object + when InstanceVerifyingDouble + message.add_line do + plain "The " + highlight doubled_module.description + plain " class does not implement the instance method " + highlight method_name.to_s + plain "." + end + + if ObjectMethodReference.for(doubled_module, method_name).implemented? + message.add_line do + plain "Perhaps you meant to use " + highlight "class_double" + plain " instead?" + end + end + when ClassVerifyingDouble + message.add_line do + plain "The " + highlight doubled_module.description + plain " class does not implement the class method " + highlight method_name.to_s + plain "." + end + + if ObjectMethodReference.for(doubled_module, method_name).implemented? + message.add_line do + plain "Perhaps you meant to use " + highlight "instance_double" + plain " instead?" + end + end + else + message.line do + blue method_name.to_s + plain " is not a method on " + yellow doubled_module.description + plain "." + end + end + + __raise message.to_s + end + + def method_call_args_description( + args, + generic_prefix = " with arguments: ", + matcher_prefix = " with ", + color: + ) + case args.first + when ArgumentMatchers::AnyArgsMatcher + [ + matcher_prefix, + SuperDiff::Helpers.style(color, "any"), + " arguments" + ].join + when ArgumentMatchers::NoArgsMatcher + [ + matcher_prefix, + SuperDiff::Helpers.style(color, "no"), + " arguments" + ].join + else + if yield + [ + generic_prefix, + SuperDiff::Helpers.style(color, format_args(args)) + ].join + else + "" + end + end + end + + def intro(unwrapped=false) + case @target + when TestDouble then TestDoubleFormatter.format(@target, unwrapped) + when Class then @target.name + when NilClass then "nil" + else "#<#{@target.class.name}>" + end + end + + private + + def received_part_of_expectation_error(actual_received_count, args) + rest = count_message(actual_received_count, color: :beta) + + if actual_received_count > 0 + rest << method_call_args_description(args, color: :beta) do + args.length > 0 + end + end + + "Received: #{rest}" + end + + def expected_part_of_expectation_error(expected_received_count, expectation_count_type, argument_list_matcher) + rest = [ + count_message( + expected_received_count, expectation_count_type, + color: :alpha + ), + method_call_args_description( + argument_list_matcher.expected_args, + color: :alpha + ) do + argument_list_matcher.expected_args.length > 0 + end + ].join + + "Expected: #{rest}" + end + + def error_message(expectation, args_for_multiple_calls) + expected_args = SuperDiff::Helpers.style( + :alpha, + format_args(expectation.expected_args) + ) + actual_args = format_received_args(args_for_multiple_calls) + message = default_error_message(expectation, expected_args, actual_args) + + if args_for_multiple_calls.one? + diff = diff_message( + expectation.expected_args, + args_for_multiple_calls.first + ) + + unless diff.strip.empty? + message << "\n\n" + message << SuperDiff::DiffLegendBuilder.call(expectation.expected_args).to_s + message << diff + end + end + + message + end + + def format_received_args(args_for_multiple_calls) + grouped_args(args_for_multiple_calls).map do |args_for_one_call, index| + SuperDiff::Helpers.style(:beta, format_args(args_for_one_call)) + + group_count(index, args_for_multiple_calls, color: :beta) + end.join("\n ") + end + + def count_message(count, expectation_count_type=nil, color:) + if count < 0 || expectation_count_type == :at_least + times(count.abs, color: color, prefix: "at least ") + end + + if expectation_count_type == :at_most + times(count.abs, color: color, prefix: "at most ") + end + + times(count, color: color) + end + + def group_count(index, args, color:) + " (#{times(index, color: color)})" if args.size > 1 || index > 1 + end + + def times(count, color:, prefix: '') + SuperDiff::Helpers.style(color, "#{prefix}#{count}") + + " time#{count == 1 ? '' : 's'}" + end + + def differ + SuperDiff::RSpec::Differ + end + end + end end diff --git a/lib/super_diff/rspec/object_inspection/inspectors.rb b/lib/super_diff/rspec/object_inspection/inspectors.rb index 6db9b00c..7cbe6041 100644 --- a/lib/super_diff/rspec/object_inspection/inspectors.rb +++ b/lib/super_diff/rspec/object_inspection/inspectors.rb @@ -2,6 +2,10 @@ module SuperDiff module RSpec module ObjectInspection module Inspectors + autoload( + :ArrayIncludingArgument, + "super_diff/rspec/object_inspection/inspectors/array_including_argument", + ) autoload( :CollectionContainingExactly, "super_diff/rspec/object_inspection/inspectors/collection_containing_exactly", @@ -10,10 +14,30 @@ module Inspectors :CollectionIncluding, "super_diff/rspec/object_inspection/inspectors/collection_including", ) + autoload( + :DuckTypeArgument, + "super_diff/rspec/object_inspection/inspectors/duck_type_argument", + ) + autoload( + :HashExcludingArgument, + "super_diff/rspec/object_inspection/inspectors/hash_excluding_argument", + ) autoload( :HashIncluding, "super_diff/rspec/object_inspection/inspectors/hash_including", ) + autoload( + :HashIncludingArgument, + "super_diff/rspec/object_inspection/inspectors/hash_including_argument", + ) + autoload( + :KindOfArgument, + "super_diff/rspec/object_inspection/inspectors/kind_of_argument", + ) + autoload( + :Matcher, + "super_diff/rspec/object_inspection/inspectors/matcher", + ) autoload( :ObjectHavingAttributes, "super_diff/rspec/object_inspection/inspectors/object_having_attributes", diff --git a/lib/super_diff/rspec/object_inspection/inspectors/array_including_argument.rb b/lib/super_diff/rspec/object_inspection/inspectors/array_including_argument.rb new file mode 100644 index 00000000..82d9e7ce --- /dev/null +++ b/lib/super_diff/rspec/object_inspection/inspectors/array_including_argument.rb @@ -0,0 +1,21 @@ +module SuperDiff + module RSpec + module ObjectInspection + module Inspectors + ArrayIncludingArgument = + SuperDiff::ObjectInspection::InspectionTree.new do + add_text "array_including(" + + nested do |matcher| + insert_array_inspection_of( + matcher.instance_variable_get("@expected"), + ) + end + + add_break + add_text ")" + end + end + end + end +end diff --git a/lib/super_diff/rspec/object_inspection/inspectors/duck_type_argument.rb b/lib/super_diff/rspec/object_inspection/inspectors/duck_type_argument.rb new file mode 100644 index 00000000..a95481d9 --- /dev/null +++ b/lib/super_diff/rspec/object_inspection/inspectors/duck_type_argument.rb @@ -0,0 +1,21 @@ +module SuperDiff + module RSpec + module ObjectInspection + module Inspectors + DuckTypeArgument = + SuperDiff::ObjectInspection::InspectionTree.new do + add_text "duck_type(" + + nested do |matcher| + insert_array_inspection_of( + matcher.instance_variable_get("@methods_to_respond_to"), + ) + end + + add_break + add_text ")" + end + end + end + end +end diff --git a/lib/super_diff/rspec/object_inspection/inspectors/hash_excluding_argument.rb b/lib/super_diff/rspec/object_inspection/inspectors/hash_excluding_argument.rb new file mode 100644 index 00000000..211e00fa --- /dev/null +++ b/lib/super_diff/rspec/object_inspection/inspectors/hash_excluding_argument.rb @@ -0,0 +1,22 @@ +module SuperDiff + module RSpec + module ObjectInspection + module Inspectors + HashExcludingArgument = + SuperDiff::ObjectInspection::InspectionTree.new do + add_text "hash_not_including(" + + nested do |matcher| + insert_hash_inspection_of( + matcher.instance_variable_get("@expected"), + initial_break: nil, + ) + end + + add_break + add_text ")" + end + end + end + end +end diff --git a/lib/super_diff/rspec/object_inspection/inspectors/hash_including_argument.rb b/lib/super_diff/rspec/object_inspection/inspectors/hash_including_argument.rb new file mode 100644 index 00000000..9f76969f --- /dev/null +++ b/lib/super_diff/rspec/object_inspection/inspectors/hash_including_argument.rb @@ -0,0 +1,22 @@ +module SuperDiff + module RSpec + module ObjectInspection + module Inspectors + HashIncludingArgument = + SuperDiff::ObjectInspection::InspectionTree.new do + add_text "hash_including(" + + nested do |matcher| + insert_hash_inspection_of( + matcher.instance_variable_get("@expected"), + initial_break: nil, + ) + end + + add_break + add_text ")" + end + end + end + end +end diff --git a/lib/super_diff/rspec/object_inspection/inspectors/kind_of_argument.rb b/lib/super_diff/rspec/object_inspection/inspectors/kind_of_argument.rb new file mode 100644 index 00000000..3358a75f --- /dev/null +++ b/lib/super_diff/rspec/object_inspection/inspectors/kind_of_argument.rb @@ -0,0 +1,14 @@ +module SuperDiff + module RSpec + module ObjectInspection + module Inspectors + KindOfArgument = + SuperDiff::ObjectInspection::InspectionTree.new do + add_text do |matcher| + "(kind of #{matcher.instance_variable_get("@klass")})" + end + end + end + end + end +end diff --git a/lib/super_diff/rspec/object_inspection/inspectors/matcher.rb b/lib/super_diff/rspec/object_inspection/inspectors/matcher.rb new file mode 100644 index 00000000..5df5503f --- /dev/null +++ b/lib/super_diff/rspec/object_inspection/inspectors/matcher.rb @@ -0,0 +1,15 @@ +module SuperDiff + module RSpec + module ObjectInspection + module Inspectors + Matcher = SuperDiff::ObjectInspection::InspectionTree.new do + # rubocop:disable Style/SymbolProc + add_text do |object| + object.description + end + # rubocop:enable Style/SymbolProc + end + end + end + end +end diff --git a/lib/super_diff/rspec/object_inspection/map_extension.rb b/lib/super_diff/rspec/object_inspection/map_extension.rb index 995c5bc9..ecdee7a1 100644 --- a/lib/super_diff/rspec/object_inspection/map_extension.rb +++ b/lib/super_diff/rspec/object_inspection/map_extension.rb @@ -13,10 +13,29 @@ def call(object) Inspectors::CollectionContainingExactly elsif object.is_a?(::RSpec::Mocks::Double) SuperDiff::ObjectInspection::Inspectors::Primitive + elsif object.is_a?(::RSpec::Mocks::ArgumentMatchers::ArrayIncludingMatcher) + Inspectors::ArrayIncludingArgument + elsif object.is_a?(::RSpec::Mocks::ArgumentMatchers::DuckTypeMatcher) + Inspectors::DuckTypeArgument + elsif object.is_a?(::RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher) + Inspectors::HashExcludingArgument + elsif object.is_a?(::RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher) + Inspectors::HashIncludingArgument + elsif object.is_a?(::RSpec::Mocks::ArgumentMatchers::KindOf) + Inspectors::KindOfArgument + elsif matcher?(object) + Inspectors::Matcher else super end end + + private + + def matcher?(object) + ::RSpec::Support.is_a_matcher?(object) && + object.respond_to?(:description) + end end end end diff --git a/lib/super_diff/rspec/string_tagger.rb b/lib/super_diff/rspec/string_tagger.rb new file mode 100644 index 00000000..181a7d56 --- /dev/null +++ b/lib/super_diff/rspec/string_tagger.rb @@ -0,0 +1,134 @@ +module SuperDiff + module RSpec + class StringTagger + extend AttrExtras.mixin + + method_object [:string!, :regex!, :colors!] + + def call + before_first_capture + + captures_and_surrounding_non_captures + + after_last_capture + end + + private + + def before_first_capture + if captures.first.begin > 0 + [ + NonCapture.new( + string, + begin: 0, + end: captures.first.begin - 1, + ), + ] + else + [] + end + end + + def captures_and_surrounding_non_captures + (0..captures.length - 1).reduce([]) do |segments, index| + capture = captures[index] + next_capture = captures[index + 1] + + segments << capture + + if next_capture && next_capture.begin > capture.end + 1 + segments << NonCapture.new( + string, + begin: capture.end + 1, + end: next_capture.begin - 1, + ) + end + + segments + end + end + + def after_last_capture + if captures.last.end < string.length - 1 + [ + NonCapture.new( + string, + begin: captures.last.end + 1, + end: string.length - 1, + ), + ] + else + [] + end + end + + def captures + @_captures ||= capture_offsets.map.with_index do |offset, index| + Capture.new( + string, + begin: offset.begin, + end: offset.end, + color: colors.fetch(index), + ) + end + end + + def capture_offsets + @_capture_offsets ||= match.captures.size.times.map do |i| + # rubocop:disable Lint/UnderscorePrefixedVariableName + _begin, _end = match.offset(i + 1) + # rubocop:enable Lint/UnderscorePrefixedVariableName + Range.new(_begin, _end - 1) + end + end + + def match + @_match ||= begin + match = regex.match(string) + + if !match + raise "Regex doesn't match!" + end + + if match.captures.empty? + raise "There are no captures!" + end + + match + end + end + + class Segment + attr_reader :string, :begin, :end, :portion_of_message + + def initialize(string, args) + @string = string + @begin = args.fetch(:begin) + @end = args.fetch(:end) + @portion_of_message = string[@begin..@end] + end + + def to_s + raise NotImplementedError + end + end + + class NonCapture < Segment + def to_s + portion_of_message + end + end + + class Capture < Segment + attr_reader :color + + def initialize(string, color:, **rest) + super(string, **rest) + @color = color + end + + def to_s + SuperDiff::Helpers.style(color, portion_of_message) + end + end + end + end +end diff --git a/spec/integration/rspec/have_received_spec.rb b/spec/integration/rspec/have_received_spec.rb new file mode 100644 index 00000000..5232b403 --- /dev/null +++ b/spec/integration/rspec/have_received_spec.rb @@ -0,0 +1,253 @@ +require "spec_helper" + +RSpec.describe "Integration with RSpec's #have_received matcher", type: :integration do + # .to receive(...).with(any_args) + # .to receive(...).with(no_args) + # .to receive(...).with(...) + # ^ any of that + .once or .twice or .times(...) or .never + # and the method can be called never or with no args (expecting some args) or + # with different args (expecting no or some args) + # or what happens if the method doesn't exist? + # also, you can do all of this on an object that is either partially doubled, + # or a full double itself + + context "when used against a partially-doubled object" do + context "and the matcher is not qualified with anything" do + context "and the method is never called" do + it "produces the correct output when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo) + expect(object).to have_received(:foo) + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|expect(object).to have_received(:foo)|, + newline_before_expectation: true, + expectation: proc { + line do + highlight "(#).foo(*(any args))" + end + + line indent_by: 2 do + plain "expected: " + alpha "1" + plain " time with " + alpha "any" + plain " arguments" + end + + line indent_by: 2 do + plain "received: " + beta "0" + plain " times with " + beta "any" + plain " arguments" + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + end + + context "and the method is called" do + it "produces the correct output when used in the negative" + end + end + + context "and matcher is qualified with .with" do + # context "and any_args" do + # end + + context "and no_args" do + context "and the method is called with some arguments" do + # this test is already done below + xit "produces the correct output when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo) + object.foo('bar', 'baz') + expect(object).to have_received(:foo).with(no_args) + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|expect(object).to have_received(:foo).with(no_args)|, + newline_before_expectation: true, + expectation: proc { + line do + highlight "#" + plain " received " + highlight ":foo" + plain " with unexpected arguments" + end + + line indent_by: 2 do + plain "expected: " + alpha %|(no args)| + end + + line indent_by: 2 do + plain " got: " + beta %|("bar", "baz")| + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + end + + context "and the method is called with no arguments" do + it "produces the correct output when used in the negative" + end + end + + context "and some number of arguments" do + context "and the method is called with no arguments" + + context "and the method is called with different arguments" do + context "and the method is only called once" do + it "produces the correct output when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo) + object.foo('bar', 'baz') + expect(object).to have_received(:foo).with('qux') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|expect(object).to have_received(:foo).with('qux')|, + newline_before_expectation: true, + expectation: proc { + line do + highlight "#" + plain " received " + highlight ":foo" + plain " with unexpected arguments" + end + + line indent_by: 2 do + plain "expected: " + alpha %|("qux")| + end + + line indent_by: 2 do + plain " got: " + beta %|("bar", "baz")| + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + end + + context "and the method is called multiple times" do + it "produces the correct output when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo) + object.foo('bar', 'baz') + object.foo('qux', 'blargh') + expect(object).to have_received(:foo).with('qux') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|expect(object).to have_received(:foo).with('qux')|, + newline_before_expectation: true, + expectation: proc { + line do + highlight "#" + plain " received " + highlight ":foo" + plain " with unexpected arguments" + end + + line indent_by: 2 do + plain "expected: " + alpha %|("qux")| + end + + line indent_by: 2 do + plain " got: " + beta %|("bar", "baz")| + plain " (" + beta "1" + plain " time)" + end + + line indent_by: 2 do + plain " " + beta %|("qux", "blargh")| + plain " (" + beta "1" + plain " time)" + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + end + end + + context "and the method is called with the same arguments" do + it "produces the correct output when used in the negative" + end + end + end + end +end diff --git a/spec/integration/rspec/receive_spec.rb b/spec/integration/rspec/receive_spec.rb new file mode 100644 index 00000000..38853aab --- /dev/null +++ b/spec/integration/rspec/receive_spec.rb @@ -0,0 +1,515 @@ +require "spec_helper" + +# TODO: Should this reverse received vs. expected? +RSpec.describe "Integration with RSpec's #receive matcher", type: :integration do + # with verifying doubles on vs. off + # using: allow vs. expect + # used against: a partially-doubled object vs. a double + # method exists vs. doesn't exist + # method is public vs. private or protected + # qualified with: + # - `with` + # * and any_args: no args in positive vs. any args in negative + # * and no_args: some args in positive vs. no args in negative + # * and some args: no args in positive vs. different args in positive vs. + # same args in negative + # - `and_return`: different rv in positive vs. same rv in negative + # - nothing: some args in positive vs. no args in negative + # + # with verifying doubles on: + # + # - what arguments was the matcher given? + # - what arguments does the method take? -- if this is different from matcher + # then instafail + # - what arguments was the method ultimately called with? + + context "with verifying doubles enabled" do + around do |example| + previous_verify_doubles = + RSpec::Mocks.configuration.verify_partial_doubles? + RSpec::Mocks.configuration.verify_partial_doubles = true + + example.run + + RSpec::Mocks.configuration.verify_partial_doubles = + previous_verify_doubles + end + + context "using allow" do + context "when used in the positive" do + context "when used against a partially-doubled object" do + context "when the method exists" do + context "and is public" do + context "and the matcher is qualified with .with" do + context "+ any_args" do + context "and the method takes no arguments" do + context "but the method is called with some arguments" do + it "raises an ArgumentError" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo).with(any_args) + object.foo('bar', 'baz') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|object.foo('bar', 'baz')|, + newline_before_expectation: true, + indentation: 5, + expectation: proc { + line do + red "ArgumentError:" + end + + red_line indent_by: 2 do + text "Wrong number of arguments. " + text "Expected 0, got 2." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + end + + xcontext "and the method takes arguments" do + it "passes" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo(one, two); end + end + object = B.new + allow(object).to receive(:foo).with(any_args) + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|allow(object).to receive(:foo).any_args|, + expectation: proc { + red_line do + text "Wrong number of arguments. " + text "Expected 2, got 0." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + end + + context "+ no_args" do + context "and the method takes no arguments" do + context "but the method is called with some arguments" do + it "raises an ArgumentError" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo).with(no_args) + object.foo('bar', 'baz') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|object.foo('bar', 'baz')|, + newline_before_expectation: true, + indentation: 5, + expectation: proc { + line do + red "ArgumentError:" + end + + red_line indent_by: 2 do + text "Wrong number of arguments. " + text "Expected 0, got 2." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + end + + context "and the method takes arguments" do + it "raises an ArgumentError" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo(one, two); end + end + object = B.new + allow(object).to receive(:foo).with(no_args) + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|allow(object).to receive(:foo).with(no_args)|, + expectation: proc { + red_line do + text "Wrong number of arguments. " + text "Expected 2, got 0." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + end + + context "+ some number of arguments" do + context "and the method takes no arguments" do + it "raises an ArgumentError" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + allow(object).to receive(:foo).with('foo', 'bar') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|allow(object).to receive(:foo).with('foo', 'bar')|, + expectation: proc { + red_line do + text "Wrong number of arguments. " + text "Expected 0, got 2." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + + context "and the method takes some arguments" do + context "and is stubbed with a different number of arguments" + end + + context "and the method takes a different number of arguments" do + it "raises an ArgumentError" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo(arg); end + end + object = B.new + allow(object).to receive(:foo).with('foo', 'bar') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|allow(object).to receive(:foo).with('foo', 'bar')|, + expectation: proc { + red_line do + text "Wrong number of arguments. " + text "Expected 1, got 2." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + + context "and the method takes arguments" do + context "and the method is called with the same number of arguments, but different values" do + it "fails with the correct output" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo(one, two); end + end + object = B.new + allow(object).to receive(:foo).with('foo', 'bar') + object.foo('baz', 'qux') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|object.foo('baz', 'qux')|, + newline_before_expectation: true, + expectation: proc { + red_line( + "# received #foo with unexpected arguments.", + ) + + newline + + line do + plain "Expected: " + alpha %|("foo", "bar")| + end + + line do + plain " Got: " + beta %|("baz", "qux")| + end + }, + diff: proc { + plain_line %! [! + alpha_line %!- "foo",! + beta_line %!+ "baz",! + alpha_line %!- "bar"! + beta_line %!+ "qux"! + plain_line %! ]! + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + end + + context "and the method is called with a different number of arguments" do + it "raises an ArgumentError" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo(one); end + end + object = B.new + allow(object).to receive(:foo).with('foo', 'bar') + object.foo('baz', 'qux') + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|allow(object).to receive(:foo).with('foo', 'bar')|, + expectation: proc { + red_line do + text "Wrong number of arguments. " + text "Expected 1, got 2." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + end + + context "and the method is called with no arguments altogether" + + context "and the method is called with the same arguments" + end + end + end + end + + context "and it is protected" + + context "and it is private" + end + + context "when the method does not exist" + end + + context "when used against a double" + end + + context "when used in the negative" do + it "fails with the correct output" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + allow(Object.new).not_to receive(:foo) + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|allow(Object.new).not_to receive(:foo)|, + expectation: proc { + red_line( + "`allow(...).not_to receive` is not supported since it " + + "doesn't really make sense. What would it even mean?", + ) + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + in_color(color_enabled) + end + end + end + end + +=begin + context "using expect" do + context "when used against a partially-doubled object" do + context "and the matcher is not qualified with anything" do + context "and the method does not exist" do + it "produces the correct output when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B; end + object = B.new + expect(object).to receive(:foo) + TEST + + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|expect(object).to receive(:foo)|, + newline_before_expectation: true, + expectation: proc { + line do + red "Could not place double." + end + + newline + + line do + blue "foo" + plain " is not a method on " + yellow "#" + plain "." + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + + it "produces the correct output when used in the negative???" + end + + context "and the method is never called" do + it "produces the correct output when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + class B + def foo; end + end + object = B.new + expect(object).to receive(:foo) + TEST + program = make_plain_test_program( + snippet, + color_enabled: color_enabled, + ) + + expected_output = build_expected_output( + color_enabled: color_enabled, + snippet: %|expect(object).to receive(:foo)|, + newline_before_expectation: true, + expectation: proc { + line do + red "Expectation failed for double: B#foo" + end + + line indent_by: 2 do + plain "Expected: " + alpha "1" + plain " time with " + alpha "any" + plain " arguments" + end + + line indent_by: 2 do + plain "Received: " + beta "0" + plain " times" + end + }, + ) + + expect(program). + to produce_output_when_run(expected_output). + removing_object_ids. + in_color(color_enabled) + end + end + + it "produces the correct output when used in the negative???" + end + + context "and the method is called" do + it "produces the correct output when used in the negative" + end + end + end + end +=end + end +end diff --git a/spec/support/integration/helpers.rb b/spec/support/integration/helpers.rb index 435e9443..b916b833 100644 --- a/spec/support/integration/helpers.rb +++ b/spec/support/integration/helpers.rb @@ -84,7 +84,12 @@ module IntegrationTests; end end RSpec.configure do |config| + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + config.color_mode = :#{color_enabled ? "on" : "off"} + config.include SuperDiff::IntegrationTests end @@ -114,7 +119,8 @@ def build_expected_output( expectation:, newline_before_expectation: false, indentation: 7, - diff: nil + diff: nil, + after_diff: nil ) colored(color_enabled: color_enabled) do line "Failures:\n" @@ -131,7 +137,7 @@ def build_expected_output( end indent by: indentation do - evaluate_block(&expectation) + apply(&expectation) if diff newline @@ -168,9 +174,13 @@ def build_expected_output( newline - evaluate_block(&diff) + apply(&diff) newline + + if after_diff + apply(&after_diff) + end end end end diff --git a/spec/support/integration/matchers/produce_output_when_run_matcher.rb b/spec/support/integration/matchers/produce_output_when_run_matcher.rb index 727b75ff..527ebdf6 100644 --- a/spec/support/integration/matchers/produce_output_when_run_matcher.rb +++ b/spec/support/integration/matchers/produce_output_when_run_matcher.rb @@ -48,13 +48,17 @@ def failure_message "Actual output:\n\n" + CommandRunner::OutputHelpers.bookended(actual_output) - ::RSpec::Matchers::ExpectedsForMultipleDiffs. - from(expected_output). - message_with_diff( - message, - ::RSpec::Expectations.differ, - actual_output, - ) + if ENV["SHOW_DIFF"] + ::RSpec::Matchers::ExpectedsForMultipleDiffs. + from(expected_output). + message_with_diff( + message, + ::RSpec::Expectations.differ, + actual_output, + ) + else + message + end end end diff --git a/spec/unit/object_inspection_spec.rb b/spec/unit/object_inspection_spec.rb index b7ac923f..b90cd777 100644 --- a/spec/unit/object_inspection_spec.rb +++ b/spec/unit/object_inspection_spec.rb @@ -589,7 +589,7 @@ as_single_line: true, ) expect(inspection).to match( - /#/ + /#/, ) end end @@ -601,7 +601,7 @@ as_single_line: false, ) expect(inspection).to match( - /#/ + /#/, ) end end @@ -772,7 +772,9 @@ ) expect(inspection).to eq( - %(#) + # rubocop:disable Metrics/LineLength + %(#), + # rubocop:enable Metrics/LineLength ) end end @@ -816,7 +818,9 @@ ) expect(inspection).to eq( - %(#, #]>) + # rubocop:disable Metrics/LineLength + %(#, #]>), + # rubocop:enable Metrics/LineLength ) end end @@ -859,17 +863,21 @@ context "given as_single_line: true" do it "returns a representation of the object on a single line" do inspection = described_class.inspect( + # rubocop:disable Style/BracesAroundHashParameters HashWithIndifferentAccess.new({ line_1: "123 Main St.", city: "Hill Valley", state: "CA", zip: "90382", }), + # rubocop:enable Style/BracesAroundHashParameters as_single_line: true, ) expect(inspection).to eq( - %(# "123 Main St.", "city" => "Hill Valley", "state" => "CA", "zip" => "90382" }>) + # rubocop:disable Metrics/LineLength + %(# "123 Main St.", "city" => "Hill Valley", "state" => "CA", "zip" => "90382" }>), + # rubocop:enable Metrics/LineLength ) end end @@ -877,12 +885,14 @@ context "given as_single_line: false" do it "returns a representation of the object across multiple lines" do inspection = described_class.inspect( + # rubocop:disable Style/BracesAroundHashParameters HashWithIndifferentAccess.new({ line_1: "123 Main St.", city: "Hill Valley", state: "CA", zip: "90382", }), + # rubocop:enable Style/BracesAroundHashParameters as_single_line: false, ) @@ -1057,31 +1067,282 @@ end end end - end - context "given a data structure that refers to itself somewhere inside of it" do - context "given as_single_line: true" do - it "replaces the reference with ∙∙∙" do - value = ["a", "b", "c"] - value.insert(1, value) - inspection = described_class.inspect(value, as_single_line: true) - expect(inspection).to eq(%(["a", ∙∙∙, "b", "c"])) + context "given a data structure that refers to itself somewhere inside of it" do + context "given as_single_line: true" do + it "replaces the reference with ∙∙∙" do + value = ["a", "b", "c"] + value.insert(1, value) + inspection = described_class.inspect(value, as_single_line: true) + expect(inspection).to eq(%(["a", ∙∙∙, "b", "c"])) + end + end + + context "given as_single_line: false" do + it "replaces the reference with ∙∙∙" do + value = ["a", "b", "c"] + value.insert(1, value) + inspection = described_class.inspect(value, as_single_line: false) + expect(inspection).to eq(<<~INSPECTION.rstrip) + [ + "a", + ∙∙∙, + "b", + "c" + ] + INSPECTION + end end end - context "given as_single_line: false" do - it "replaces the reference with ∙∙∙" do - value = ["a", "b", "c"] - value.insert(1, value) - inspection = described_class.inspect(value, as_single_line: false) - expect(inspection).to eq(<<~INSPECTION.rstrip) - [ - "a", - ∙∙∙, - "b", - "c" - ] - INSPECTION + context "given an AnyArgMatcher" do + context "given as_single_line: true" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(anything, as_single_line: true) + expect(inspection).to eq("anything") + end + end + + context "given as_single_line: false" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(anything, as_single_line: false) + expect(inspection).to eq("anything") + end + end + end + + context "given an AnyArgsMatcher" do + context "given as_single_line: true" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(any_args, as_single_line: true) + expect(inspection).to eq("*(any args)") + end + end + + context "given as_single_line: false" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(any_args, as_single_line: false) + expect(inspection).to eq("*(any args)") + end + end + end + + context "given an ArrayIncludingMatcher" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + array_including(1, 2, 3), + as_single_line: true, + ) + expect(inspection).to eq(%[array_including(1, 2, 3)]) + end + end + + context "given as_single_line: false" do + it "returns a representation of the object across multiple lines" do + inspection = described_class.inspect( + array_including(1, 2, 3), + as_single_line: false, + ) + expect(inspection).to eq(<<~INSPECTION.rstrip) + array_including( + 1, + 2, + 3 + ) + INSPECTION + end + end + end + + context "given a BooleanMatcher" do + context "given as_single_line: true" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(boolean, as_single_line: true) + expect(inspection).to eq("boolean") + end + end + + context "given as_single_line: false" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(boolean, as_single_line: false) + expect(inspection).to eq("boolean") + end + end + end + + context "given a DuckTypeMatcher" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + duck_type(:foo, :bar, :baz), + as_single_line: true, + ) + expect(inspection).to eq(%[duck_type(:foo, :bar, :baz)]) + end + end + + context "given as_single_line: false" do + it "returns a representation of the object across multiple lines" do + inspection = described_class.inspect( + duck_type(:foo, :bar, :baz), + as_single_line: false, + ) + expect(inspection).to eq(<<~INSPECTION.rstrip) + duck_type( + :foo, + :bar, + :baz + ) + INSPECTION + end + end + end + + context "given a HashExcludingMatcher" do + context "via #hash_excluding" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + hash_excluding(foo: "bar", baz: "qux"), + as_single_line: true, + ) + expect(inspection).to eq( + %[hash_not_including(foo: "bar", baz: "qux")], + ) + end + end + + context "given as_single_line: false" do + it "returns a representation of the object across multiple lines" do + inspection = described_class.inspect( + hash_excluding(foo: "bar", baz: "qux"), + as_single_line: false, + ) + expect(inspection).to eq(<<~INSPECTION.rstrip) + hash_not_including( + foo: "bar", + baz: "qux" + ) + INSPECTION + end + end + end + + context "via #hash_not_including" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + hash_not_including(foo: "bar", baz: "qux"), + as_single_line: true, + ) + expect(inspection).to eq( + %[hash_not_including(foo: "bar", baz: "qux")], + ) + end + end + + context "given as_single_line: false" do + it "returns a representation of the object across multiple lines" do + inspection = described_class.inspect( + hash_not_including(foo: "bar", baz: "qux"), + as_single_line: false, + ) + expect(inspection).to eq(<<~INSPECTION.rstrip) + hash_not_including( + foo: "bar", + baz: "qux" + ) + INSPECTION + end + end + end + end + + context "given a HashIncludingMatcher" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + hash_including(foo: "bar", baz: "qux"), + as_single_line: true, + ) + expect(inspection).to eq(%[hash_including(foo: "bar", baz: "qux")]) + end + end + + context "given as_single_line: false" do + it "returns a representation of the object across multiple lines" do + inspection = described_class.inspect( + hash_including(foo: "bar", baz: "qux"), + as_single_line: false, + ) + expect(inspection).to eq(<<~INSPECTION.rstrip) + hash_including( + foo: "bar", + baz: "qux" + ) + INSPECTION + end + end + end + + context "given an InstanceOf" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + instance_of(String), + as_single_line: true, + ) + expect(inspection).to eq(%[an_instance_of(String)]) + end + end + + context "given as_single_line: false" do + it "still returns a representation of the object on a single line" do + inspection = described_class.inspect( + instance_of(String), + as_single_line: false, + ) + expect(inspection).to eq(%[an_instance_of(String)]) + end + end + end + + context "given a KindOf" do + context "given as_single_line: true" do + it "returns a representation of the object on a single line" do + inspection = described_class.inspect( + kind_of(String), + as_single_line: true, + ) + expect(inspection).to eq(%[(kind of String)]) + end + end + + context "given as_single_line: false" do + it "still returns a representation of the object on a single line" do + inspection = described_class.inspect( + kind_of(String), + as_single_line: false, + ) + expect(inspection).to eq(%[(kind of String)]) + end + end + end + + context "given a NoArgsMatcher" do + context "given as_single_line: true" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(no_args, as_single_line: true) + expect(inspection).to eq("no args") + end + end + + context "given as_single_line: false" do + it "returns what the object's #inspect method returns" do + inspection = described_class.inspect(no_args, as_single_line: false) + expect(inspection).to eq("no args") + end end end end