rspec-inline-snapshot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []