rspec-inline-snapshot 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 01661d8bb47e00451b851af3d426cc0bc2e0dd5a1ad6a087b2bf036fe0260fd2
4
+ data.tar.gz: d9bcd6ddb63f4b6234e29fe10933abc60e13c4e8fc73573125d04cee83c0668b
5
+ SHA512:
6
+ metadata.gz: 50ff7415887cf95c6f04cf7e5d1655fcd984f70f9673f9eab20c650a45a1427e52328c9f6642621d9dbb6e8baef752557d4c6988003d04958d6d10e8433f9c8f
7
+ data.tar.gz: 0b38196c74116b3a432924c12aaab36fb2f81a010eb62b7d96a094235e5dd839bc495aa22906c0fa4403b9184aac9418ab0419ca40970470bb86bce7c46e45dd
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2022 Hummingbird RegTech, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module RSpec
17
+ module InlineSnapshot
18
+ module Matchers
19
+ extend RSpec::Matchers::DSL
20
+
21
+ RSpec.configure do |config|
22
+ config.after(:suite) do
23
+ REWRITERS.each do |source_file_path, (_parsed_source, corrector)|
24
+ File.write(source_file_path, corrector.process)
25
+ end
26
+
27
+ REWRITERS.clear
28
+ end
29
+ end
30
+
31
+ UNDEFINED_EXPECTED_VALUE = :_inline_snapshot_undefined
32
+ # FIXME: there's probably a way to do this without abusing a constant but it works for now
33
+ # file => [parsed_source, corrector]
34
+ REWRITERS = {}
35
+
36
+ FALSE_VALUES = [
37
+ nil,
38
+ '',
39
+ '0',
40
+ 'f',
41
+ 'false',
42
+ 'off'
43
+ ].to_set.freeze
44
+
45
+ # This is fiddly. The AST is a bit annoying in that the #location and/or #source_range of
46
+ # a SendNode are only the expression, and do not consistently include recursive children in the range.
47
+ # So we have to do the math ourselves... case by case. We aim for the range to start
48
+ # at the open parentheses (if there is one) and end at the close parentheses or heredoc terminator.
49
+ def replacement_range(node, parsed_source)
50
+ matcher_arg = node.arguments.last
51
+
52
+ if matcher_arg.nil? && node.location.begin.nil?
53
+ # no-args command-style call. ie. "match_inline_snapshot"
54
+ Parser::Source::Range.new(
55
+ parsed_source.buffer,
56
+ node.location.expression.end_pos,
57
+ node.location.expression.end_pos
58
+ )
59
+ elsif matcher_arg.is_a?(RuboCop::AST::StrNode) && matcher_arg.heredoc?
60
+ # with heredoc parameter ie. "match_inline_snapshot(<<~SNAP) ... SNAP"
61
+ Parser::Source::Range.new(
62
+ parsed_source.buffer,
63
+ node.location.begin.begin_pos,
64
+ node.arguments.last.location.heredoc_end.end_pos
65
+ )
66
+ elsif matcher_arg.is_a?(RuboCop::AST::SendNode) &&
67
+ matcher_arg.receiver.heredoc? &&
68
+ matcher_arg.method_name == :chomp
69
+ # with chomped heredoc parameter ie. "match_inline_snapshot(<<~SNAP.chomp) ... SNAP"
70
+ Parser::Source::Range.new(
71
+ parsed_source.buffer,
72
+ node.location.begin.begin_pos,
73
+ matcher_arg.receiver.location.heredoc_end.end_pos
74
+ )
75
+ else
76
+ # no-args call with parens. ie. "match_inline_snapshot()"
77
+ # or with a plain old parameter ie. "match_inline_snapshot('foo')"
78
+ Parser::Source::Range.new(
79
+ parsed_source.buffer,
80
+ node.location.begin.begin_pos,
81
+ node.location.end.end_pos
82
+ )
83
+ end
84
+ end
85
+
86
+ # Format the actual value so it can be injected into the source as the new argument.
87
+ def format_replacement(actual, node, parsed_source)
88
+ if actual.is_a?(String)
89
+ if actual.include?("\n")
90
+ indent = parsed_source.line_indentation(node.location.first_line)
91
+ [
92
+ '(<<~SNAP.chomp)',
93
+ actual.split("\n", -1).map { |line| "#{' ' * (indent + 2)}#{line}" },
94
+ "#{' ' * indent}SNAP"
95
+ ].join("\n")
96
+ else
97
+ "(#{actual.inspect})"
98
+ end
99
+ elsif actual.is_a?(NilClass) || actual.is_a?(Integer) || actual.is_a?(TrueClass) || actual.is_a?(FalseClass)
100
+ "(#{actual.inspect})"
101
+ elsif actual.respond_to?(:as_json)
102
+ "(#{actual.as_json.inspect})"
103
+ else
104
+ raise ArgumentError,
105
+ "Cannot snapshot. Actual (#{actual.class}) is not a String and does not implement #as_json"
106
+ end
107
+ end
108
+
109
+ def should_update_inline_snapshot?(expected)
110
+ env_to_boolean(ENV['UPDATE_MATCH_SNAPSHOT']) ||
111
+ env_to_boolean(ENV['UPDATE_SNAPSHOTS']) ||
112
+ expected == UNDEFINED_EXPECTED_VALUE
113
+ end
114
+
115
+ def running_in_ci?
116
+ env_to_boolean(ENV['CI'])
117
+ end
118
+
119
+ def match_or_update_inline_snapshot(matcher_name, expected, actual)
120
+ if should_update_inline_snapshot?(expected)
121
+ return false if running_in_ci?
122
+
123
+ # General algorithm:
124
+ # 1. Get location. RSpec.current_example.location
125
+ # 2. Crawl Kernel.caller_locations until first hit at #absolute_path from 1
126
+ # 3. Use #lineno as heuristic to find call to :match_inline_snapshot (matcher_name) in AST
127
+ # 4. Rewrite first argument of method call
128
+ source_file_path = RSpec.current_example.metadata[:absolute_file_path]
129
+ caller_location = Kernel.caller_locations.detect do |cl|
130
+ cl.absolute_path == RSpec.current_example.metadata[:absolute_file_path]
131
+ end
132
+ matcher_call_line_number = caller_location.lineno
133
+
134
+ # Parse the spec file.
135
+ # See:
136
+ # https://www.rubydoc.info/github/whitequark/parser/Parser/TreeRewriter
137
+ # https://medium.com/flippengineering/using-rubocop-ast-to-transform-ruby-files-using-abstract-syntax-trees-3e352e9ac916
138
+ REWRITERS[source_file_path] ||= begin
139
+ parsed_source = RuboCop::AST::ProcessedSource.from_file(source_file_path,
140
+ 2.7)
141
+ corrector = ::RuboCop::Cop::Corrector.new(parsed_source)
142
+ [parsed_source, corrector]
143
+ end
144
+
145
+ parsed_source, corrector = REWRITERS[source_file_path]
146
+
147
+ parsed_source.ast.each_node(:send) do |node|
148
+ next unless node.location.first_line >= matcher_call_line_number && node.method_name == matcher_name
149
+
150
+ # found it! well we hope anyway because we're about to blow something away!
151
+ corrector.replace(replacement_range(node, parsed_source), format_replacement(actual, node, parsed_source))
152
+
153
+ return true # we replaced the target argument and overwrote the file. no point going further in this spec file.
154
+ end
155
+ raise "possible bug in inline snapshot matcher. Did not locate call to #{matcher_name} in #{source_file_path}"
156
+ else
157
+ RSpec::Support::FuzzyMatcher.values_match?(expected, actual)
158
+ end
159
+ end
160
+
161
+ def env_to_boolean(value)
162
+ !FALSE_VALUES.include?(value&.downcase)
163
+ end
164
+
165
+ matcher :match_inline_snapshot do |expected = UNDEFINED_EXPECTED_VALUE|
166
+ diffable
167
+
168
+ match do |actual|
169
+ match_or_update_inline_snapshot(:match_inline_snapshot, expected, actual)
170
+ end
171
+
172
+ failure_message do |_actual|
173
+ if running_in_ci? && should_update_inline_snapshot?(expected)
174
+ 'cannot update snapshots in CI. Did you forget to check it in?'
175
+ else
176
+ # NB: yes this works.
177
+ # The failure_message macro gets turned into a method definition by rspec
178
+ super()
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,18 @@
1
+ # Copyright 2022 Hummingbird RegTech, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ module RSpec
15
+ module InlineSnapshot
16
+ VERSION = '1.0.0'.freeze
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright 2022 Hummingbird RegTech, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require 'rubocop/ast'
15
+ require 'rubocop/cop/corrector'
16
+
17
+ require_relative 'rspec/inline_snapshot/matchers'
18
+
19
+ RSpec.configure do |config|
20
+ config.include RSpec::InlineSnapshot::Matchers
21
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-inline-snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Hummingbird RegTech, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-ast
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Inline snapshot expectations for RSpec
70
+ email: info@hummingbird.co
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/rspec-inline-snapshot.rb
76
+ - lib/rspec/inline_snapshot/matchers.rb
77
+ - lib/rspec/inline_snapshot/version.rb
78
+ homepage: https://github.com/Hummingbird-RegTech/rspec-inline-snapshot
79
+ licenses:
80
+ - Apache-2.0
81
+ metadata:
82
+ rubygems_mfa_required: 'true'
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.7.0
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.1.6
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Inline snapshot expectations for RSpec
102
+ test_files: []