Module: RSpec::InlineSnapshot::Matchers
- Extended by:
- Matchers::DSL
- Defined in:
- lib/rspec/inline_snapshot/matchers.rb
Constant Summary collapse
- UNDEFINED_EXPECTED_VALUE =
:_inline_snapshot_undefined
- REWRITERS =
FIXME: there’s probably a way to do this without abusing a constant but it works for now file => [parsed_source, corrector]
{}
- FALSE_VALUES =
[ nil, '', '0', 'f', 'false', 'off' ].to_set.freeze
Instance Method Summary collapse
- #env_to_boolean(value) ⇒ Object
-
#format_replacement(actual, node, parsed_source) ⇒ Object
Format the actual value so it can be injected into the source as the new argument.
- #match_or_update_inline_snapshot(matcher_name, expected, actual) ⇒ Object
-
#replacement_range(node, parsed_source) ⇒ Object
This is fiddly.
- #running_in_ci? ⇒ Boolean
- #should_update_inline_snapshot?(expected) ⇒ Boolean
Instance Method Details
#env_to_boolean(value) ⇒ Object
161 162 163 |
# File 'lib/rspec/inline_snapshot/matchers.rb', line 161 def env_to_boolean(value) !FALSE_VALUES.include?(value&.downcase) end |
#format_replacement(actual, node, parsed_source) ⇒ Object
Format the actual value so it can be injected into the source as the new argument.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/rspec/inline_snapshot/matchers.rb', line 87 def format_replacement(actual, node, parsed_source) if actual.is_a?(String) if actual.include?("\n") indent = parsed_source.line_indentation(node.location.first_line) [ '(<<~SNAP.chomp)', actual.split("\n", -1).map { |line| "#{' ' * (indent + 2)}#{line}" }, "#{' ' * indent}SNAP" ].join("\n") else "(#{actual.inspect})" end elsif actual.is_a?(NilClass) || actual.is_a?(Integer) || actual.is_a?(TrueClass) || actual.is_a?(FalseClass) "(#{actual.inspect})" elsif actual.respond_to?(:as_json) "(#{actual.as_json.inspect})" else raise ArgumentError, "Cannot snapshot. Actual (#{actual.class}) is not a String and does not implement #as_json" end end |
#match_or_update_inline_snapshot(matcher_name, expected, actual) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/rspec/inline_snapshot/matchers.rb', line 119 def match_or_update_inline_snapshot(matcher_name, expected, actual) if should_update_inline_snapshot?(expected) return false if running_in_ci? # General algorithm: # 1. Get location. RSpec.current_example.location # 2. Crawl Kernel.caller_locations until first hit at #absolute_path from 1 # 3. Use #lineno as heuristic to find call to :match_inline_snapshot (matcher_name) in AST # 4. Rewrite first argument of method call source_file_path = RSpec.current_example.[:absolute_file_path] caller_location = Kernel.caller_locations.detect do |cl| cl.absolute_path == RSpec.current_example.[:absolute_file_path] end matcher_call_line_number = caller_location.lineno # Parse the spec file. # See: # https://www.rubydoc.info/github/whitequark/parser/Parser/TreeRewriter # https://medium.com/flippengineering/using-rubocop-ast-to-transform-ruby-files-using-abstract-syntax-trees-3e352e9ac916 REWRITERS[source_file_path] ||= begin parsed_source = RuboCop::AST::ProcessedSource.from_file(source_file_path, RUBY_VERSION.match(/\d+\.\d+/).to_s.to_f) corrector = ::RuboCop::Cop::Corrector.new(parsed_source) [parsed_source, corrector] end parsed_source, corrector = REWRITERS[source_file_path] parsed_source.ast.each_node(:send) do |node| next unless node.location.first_line >= matcher_call_line_number && node.method_name == matcher_name # found it! well we hope anyway because we're about to blow something away! corrector.replace(replacement_range(node, parsed_source), format_replacement(actual, node, parsed_source)) return true # we replaced the target argument and overwrote the file. no point going further in this spec file. end raise "possible bug in inline snapshot matcher. Did not locate call to #{matcher_name} in #{source_file_path}" else RSpec::Support::FuzzyMatcher.values_match?(expected, actual) end end |
#replacement_range(node, parsed_source) ⇒ Object
This is fiddly. The AST is a bit annoying in that the #location and/or #source_range of a SendNode are only the expression, and do not consistently include recursive children in the range. So we have to do the math ourselves… case by case. We aim for the range to start at the open parentheses (if there is one) and end at the close parentheses or heredoc terminator.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/rspec/inline_snapshot/matchers.rb', line 49 def replacement_range(node, parsed_source) matcher_arg = node.arguments.last if matcher_arg.nil? && node.location.begin.nil? # no-args command-style call. ie. "match_inline_snapshot" Parser::Source::Range.new( parsed_source.buffer, node.location.expression.end_pos, node.location.expression.end_pos ) elsif matcher_arg.is_a?(RuboCop::AST::StrNode) && matcher_arg.heredoc? # with heredoc parameter ie. "match_inline_snapshot(<<~SNAP) ... SNAP" Parser::Source::Range.new( parsed_source.buffer, node.location.begin.begin_pos, node.arguments.last.location.heredoc_end.end_pos ) elsif matcher_arg.is_a?(RuboCop::AST::SendNode) && matcher_arg.receiver.heredoc? && matcher_arg.method_name == :chomp # with chomped heredoc parameter ie. "match_inline_snapshot(<<~SNAP.chomp) ... SNAP" Parser::Source::Range.new( parsed_source.buffer, node.location.begin.begin_pos, matcher_arg.receiver.location.heredoc_end.end_pos ) else # no-args call with parens. ie. "match_inline_snapshot()" # or with a plain old parameter ie. "match_inline_snapshot('foo')" Parser::Source::Range.new( parsed_source.buffer, node.location.begin.begin_pos, node.location.end.end_pos ) end end |
#running_in_ci? ⇒ Boolean
115 116 117 |
# File 'lib/rspec/inline_snapshot/matchers.rb', line 115 def running_in_ci? env_to_boolean(ENV['CI']) end |
#should_update_inline_snapshot?(expected) ⇒ Boolean
109 110 111 112 113 |
# File 'lib/rspec/inline_snapshot/matchers.rb', line 109 def should_update_inline_snapshot?(expected) env_to_boolean(ENV['UPDATE_MATCH_SNAPSHOT']) || env_to_boolean(ENV['UPDATE_SNAPSHOTS']) || expected == UNDEFINED_EXPECTED_VALUE end |