From 4965ac05dbb08c6c074e42217b6e7c2fcf3f0371 Mon Sep 17 00:00:00 2001 From: Vladislav Kuznecov Date: Tue, 30 Jan 2024 07:12:21 +0300 Subject: [PATCH] feat: date like objects support (#198) Tbh, I just went through appearances of `/(TimeLike|time_like)/` and created similar logic for date like objects :D I'm really hesitant about whether `DateTime` objects should be considered `date_like` (although ActiveSupport defines `DateTime#acts_like_date?`). Since `Date` and `DateTime` are never equal, `expect(Date.new(2020, 1, 1)).to eq(DateTime.new(2020, 1, 1))` will result in a confusing diff (see the [spec/support/shared_examples/active_support.rb](https://github.com/mcmire/super_diff/pull/198/files#diff-4076c9b6c197bc5a4b90327f7efc27f9a0bc5f40ec98dfe94adf9c08f103a269R103-R143)) --- lib/super_diff.rb | 9 ++ lib/super_diff/differs.rb | 1 + lib/super_diff/differs/date_like.rb | 15 +++ lib/super_diff/differs/defaults.rb | 1 + .../inspection_tree_builders.rb | 4 + .../inspection_tree_builders/date_like.rb | 41 +++++++ .../inspection_tree_builders/defaults.rb | 1 + lib/super_diff/operation_tree_builders.rb | 1 + .../operation_tree_builders/date_like.rb | 15 +++ .../operation_tree_builders/defaults.rb | 2 +- spec/integration/rspec/eq_matcher_spec.rb | 72 ++++++++++++ .../support/shared_examples/active_support.rb | 79 +++++++++++++ .../active_support/object_inspection_spec.rb | 108 ++++++++++++++++++ spec/unit/super_diff_spec.rb | 64 +++++++++++ 14 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 lib/super_diff/differs/date_like.rb create mode 100644 lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb create mode 100644 lib/super_diff/operation_tree_builders/date_like.rb diff --git a/lib/super_diff.rb b/lib/super_diff.rb index 79edb36c..a2118b25 100644 --- a/lib/super_diff.rb +++ b/lib/super_diff.rb @@ -1,6 +1,7 @@ require "attr_extras/explicit" require "diff-lcs" require "patience_diff" +require "date" module SuperDiff autoload( @@ -57,6 +58,14 @@ def self.time_like?(value) value.is_a?(Time) end + def self.date_like?(value) + # Check for ActiveSupport's #acts_like_date? for their date-like objects + # In case class is both time-like and date-like, we should treat it as + # time-like. This is governed by the order of `Differs::DEFAULTS` entries + (value.respond_to?(:acts_like_date?) && value.acts_like_date?) || + value.is_a?(Date) + end + def self.primitive?(value) case value when true, false, nil, Symbol, Numeric, Regexp, Class diff --git a/lib/super_diff/differs.rb b/lib/super_diff/differs.rb index e5780bbf..c2aba45f 100644 --- a/lib/super_diff/differs.rb +++ b/lib/super_diff/differs.rb @@ -9,6 +9,7 @@ module Differs autoload :Main, "super_diff/differs/main" autoload :MultilineString, "super_diff/differs/multiline_string" autoload :TimeLike, "super_diff/differs/time_like" + autoload :DateLike, "super_diff/differs/date_like" end end diff --git a/lib/super_diff/differs/date_like.rb b/lib/super_diff/differs/date_like.rb new file mode 100644 index 00000000..a30f0030 --- /dev/null +++ b/lib/super_diff/differs/date_like.rb @@ -0,0 +1,15 @@ +module SuperDiff + module Differs + class DateLike < Base + def self.applies_to?(expected, actual) + SuperDiff.date_like?(expected) && SuperDiff.date_like?(actual) + end + + protected + + def operation_tree_builder_class + OperationTreeBuilders::DateLike + end + end + end +end diff --git a/lib/super_diff/differs/defaults.rb b/lib/super_diff/differs/defaults.rb index 29aaf22f..d1e58113 100644 --- a/lib/super_diff/differs/defaults.rb +++ b/lib/super_diff/differs/defaults.rb @@ -4,6 +4,7 @@ module Differs Array, Hash, TimeLike, + DateLike, MultilineString, CustomObject, DefaultObject diff --git a/lib/super_diff/object_inspection/inspection_tree_builders.rb b/lib/super_diff/object_inspection/inspection_tree_builders.rb index c42e5c84..5da89908 100644 --- a/lib/super_diff/object_inspection/inspection_tree_builders.rb +++ b/lib/super_diff/object_inspection/inspection_tree_builders.rb @@ -37,6 +37,10 @@ module InspectionTreeBuilders :TimeLike, "super_diff/object_inspection/inspection_tree_builders/time_like" ) + autoload( + :DateLike, + "super_diff/object_inspection/inspection_tree_builders/date_like" + ) end end end diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb b/lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb new file mode 100644 index 00000000..0e5d7101 --- /dev/null +++ b/lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb @@ -0,0 +1,41 @@ +module SuperDiff + module ObjectInspection + module InspectionTreeBuilders + class DateLike < Base + def self.applies_to?(value) + SuperDiff.date_like?(value) + end + + def call + InspectionTree.new do + as_lines_when_rendering_to_lines(collection_bookend: :open) do + add_text { |date| "#<#{date.class} " } + + when_rendering_to_lines { add_text "{" } + end + + when_rendering_to_string do + add_text { |date| date.strftime("%Y-%m-%d") } + end + + when_rendering_to_lines do + nested do |date| + insert_separated_list(%i[year month day]) do |name| + add_text name.to_s + add_text ": " + add_inspection_of date.public_send(name) + end + end + end + + as_lines_when_rendering_to_lines(collection_bookend: :close) do + when_rendering_to_lines { add_text "}" } + + add_text ">" + end + end + end + end + end + end +end diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb b/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb index 3f0a6e6e..53226565 100644 --- a/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb +++ b/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb @@ -7,6 +7,7 @@ module InspectionTreeBuilders Hash, Primitive, TimeLike, + DateLike, DefaultObject ].freeze end diff --git a/lib/super_diff/operation_tree_builders.rb b/lib/super_diff/operation_tree_builders.rb index 0867734f..af8b0c56 100644 --- a/lib/super_diff/operation_tree_builders.rb +++ b/lib/super_diff/operation_tree_builders.rb @@ -12,6 +12,7 @@ module OperationTreeBuilders "super_diff/operation_tree_builders/multiline_string" ) autoload :TimeLike, "super_diff/operation_tree_builders/time_like" + autoload :DateLike, "super_diff/operation_tree_builders/date_like" end end diff --git a/lib/super_diff/operation_tree_builders/date_like.rb b/lib/super_diff/operation_tree_builders/date_like.rb new file mode 100644 index 00000000..3dc091e1 --- /dev/null +++ b/lib/super_diff/operation_tree_builders/date_like.rb @@ -0,0 +1,15 @@ +module SuperDiff + module OperationTreeBuilders + class DateLike < CustomObject + def self.applies_to?(expected, actual) + SuperDiff.date_like?(expected) && SuperDiff.date_like?(actual) + end + + protected + + def attribute_names + %w[year month day] + end + end + end +end diff --git a/lib/super_diff/operation_tree_builders/defaults.rb b/lib/super_diff/operation_tree_builders/defaults.rb index 10b2f443..e3c3d797 100644 --- a/lib/super_diff/operation_tree_builders/defaults.rb +++ b/lib/super_diff/operation_tree_builders/defaults.rb @@ -1,5 +1,5 @@ module SuperDiff module OperationTreeBuilders - DEFAULTS = [Array, Hash, TimeLike, CustomObject].freeze + DEFAULTS = [Array, Hash, TimeLike, DateLike, CustomObject].freeze end end diff --git a/spec/integration/rspec/eq_matcher_spec.rb b/spec/integration/rspec/eq_matcher_spec.rb index c162b8a3..1be7a17d 100644 --- a/spec/integration/rspec/eq_matcher_spec.rb +++ b/spec/integration/rspec/eq_matcher_spec.rb @@ -252,6 +252,78 @@ end end + context "when comparing two different Date instances" do + it "produces the correct failure message when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~RUBY + expected = Date.new(2023, 10, 14) + actual = Date.new(2023, 10, 31) + expect(expected).to eq(actual) + RUBY + program = make_plain_test_program(snippet, color_enabled: color_enabled) + + expected_output = + build_expected_output( + color_enabled: color_enabled, + snippet: "expect(expected).to eq(actual)", + expectation: + proc do + line do + plain "Expected " + actual "#" + plain " to eq " + expected "#" + plain "." + end + end, + diff: + proc do + plain_line " #" + end + ) + + expect(program).to produce_output_when_run(expected_output).in_color( + color_enabled + ) + end + end + + it "produces the correct failure message when used in the negative" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~RUBY + date = Date.new(2023, 10, 14) + expect(date).not_to eq(date) + RUBY + program = make_plain_test_program(snippet, color_enabled: color_enabled) + + expected_output = + build_expected_output( + color_enabled: color_enabled, + snippet: "expect(date).not_to eq(date)", + expectation: + proc do + line do + plain "Expected " + actual "#" + plain " not to eq " + expected "#" + plain "." + end + end + ) + + expect(program).to produce_output_when_run(expected_output).in_color( + color_enabled + ) + end + end + end + context "when comparing a single-line string with a multi-line string" do it "produces the correct failure message" do as_both_colored_and_uncolored do |color_enabled| diff --git a/spec/support/shared_examples/active_support.rb b/spec/support/shared_examples/active_support.rb index 66ca455a..10a3f682 100644 --- a/spec/support/shared_examples/active_support.rb +++ b/spec/support/shared_examples/active_support.rb @@ -106,4 +106,83 @@ end end end + + context "when comparing Date instance and date-like DateTime instance for same day", + active_record: true do + it "produces the correct failure message when used in the positive" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~RUBY + expected = Date.new(2023, 10, 14) + actual = DateTime.new(2023, 10, 14, 18, 22, 26) + expect(expected).to eq(actual) + RUBY + program = + make_rspec_rails_test_program(snippet, color_enabled: color_enabled) + + expected_output = + build_expected_output( + color_enabled: color_enabled, + snippet: "expect(expected).to eq(actual)", + expectation: + proc do + line do + plain "Expected " + actual "#" + plain " to eq " + expected "#" + plain "." + end + end + ) + + expect(program).to produce_output_when_run(expected_output).in_color( + color_enabled + ) + end + end + end + + context "when comparing Date instance and date-like DateTime instance for another day", + active_record: true do + it "produces the diff for date like objects comparison" do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~RUBY + expected = Date.new(2023, 10, 14) + actual = DateTime.new(2023, 10, 31, 18, 22, 26) + expect(expected).to eq(actual) + RUBY + program = + make_rspec_rails_test_program(snippet, color_enabled: color_enabled) + + expected_output = + build_expected_output( + color_enabled: color_enabled, + snippet: "expect(expected).to eq(actual)", + expectation: + proc do + line do + plain "Expected " + actual "#" + plain " to eq " + expected "#" + plain "." + end + end, + diff: + proc do + plain_line " #" + end + ) + + expect(program).to produce_output_when_run(expected_output).in_color( + color_enabled + ) + end + end + end end diff --git a/spec/unit/active_support/object_inspection_spec.rb b/spec/unit/active_support/object_inspection_spec.rb index 448c7426..63530800 100644 --- a/spec/unit/active_support/object_inspection_spec.rb +++ b/spec/unit/active_support/object_inspection_spec.rb @@ -58,5 +58,113 @@ end end end + + context "given a DateTime object" do + context "given as_lines: false" do + it "returns a representation of the datetime on a single line" do + inspection = + described_class.inspect_object( + DateTime.new(2021, 5, 5, 10, 23, 28.123456789123, "-05:00"), + as_lines: false + ) + expect(inspection).to eq( + "#" + ) + end + end + + context "given as_lines: true" do + it "returns a representation of the datetime across multiple lines" do + inspection = + described_class.inspect_object( + DateTime.new(2021, 5, 5, 10, 23, 28.1234567891, "-05:00"), + as_lines: true, + type: :delete, + indentation_level: 1 + ) + expect(inspection).to match( + [ + an_object_having_attributes( + type: :delete, + indentation_level: 1, + value: "#", + add_comma: false, + collection_bookend: :close + ) + ] + ) + end + end + end end end diff --git a/spec/unit/super_diff_spec.rb b/spec/unit/super_diff_spec.rb index 59ab1aa3..d0cbfb17 100644 --- a/spec/unit/super_diff_spec.rb +++ b/spec/unit/super_diff_spec.rb @@ -1026,6 +1026,70 @@ end end + context "given a Date object" do + context "given as_lines: false" do + it "returns a representation of the date on a single line" do + inspection = + described_class.inspect_object( + Date.new(2023, 10, 14), + as_lines: false + ) + expect(inspection).to eq("#") + end + end + + context "given as_lines: true" do + it "returns a representation of the date across multiple lines" do + inspection = + described_class.inspect_object( + Date.new(2023, 10, 14), + as_lines: true, + type: :delete, + indentation_level: 1 + ) + expect(inspection).to match( + [ + an_object_having_attributes( + type: :delete, + indentation_level: 1, + value: "#", + add_comma: false, + collection_bookend: :close + ) + ] + ) + end + end + end + context "given a class" do context "given as_lines: false" do it "returns an inspected version of the object" do