insta 0.2.9 → 0.3.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 +4 -4
- data/LICENSE.txt +1 -1
- data/Rakefile +30 -3
- data/exe/insta +6 -0
- data/lib/insta/ansi.rb +30 -0
- data/lib/insta/ci.rb +21 -0
- data/lib/insta/cli.rb +714 -0
- data/lib/insta/config_manifest.rb +56 -0
- data/lib/insta/configuration.rb +86 -2
- data/lib/insta/diff.rb +134 -0
- data/lib/insta/inline/call_finder.rb +37 -0
- data/lib/insta/inline/edit.rb +21 -0
- data/lib/insta/inline/file_patcher.rb +269 -0
- data/lib/insta/inline/pending_registry.rb +57 -0
- data/lib/insta/inline/pending_store.rb +145 -0
- data/lib/insta/minitest/assertions.rb +254 -0
- data/lib/insta/minitest/lifecycle.rb +12 -0
- data/lib/insta/minitest.rb +8 -0
- data/lib/insta/pending_locations.rb +64 -0
- data/lib/insta/pending_reporter.rb +47 -0
- data/lib/insta/redaction/applicator.rb +89 -0
- data/lib/insta/redaction/selector.rb +193 -0
- data/lib/insta/rspec/hooks.rb +18 -0
- data/lib/insta/rspec/matchers.rb +248 -0
- data/lib/insta/rspec.rb +7 -0
- data/lib/insta/serializers/base.rb +31 -0
- data/lib/insta/serializers/inspect.rb +14 -0
- data/lib/insta/serializers/json.rb +16 -0
- data/lib/insta/serializers/string.rb +14 -0
- data/lib/insta/serializers/yaml.rb +16 -0
- data/lib/insta/snapshot.rb +74 -0
- data/lib/insta/snapshot_content.rb +44 -0
- data/lib/insta/snapshot_file.rb +64 -0
- data/lib/insta/snapshot_mismatch_handler.rb +34 -0
- data/lib/insta/snapshot_name.rb +51 -0
- data/lib/insta/syntax_highlight.rb +20 -0
- data/lib/insta/update_coordinator.rb +22 -0
- data/lib/insta/version.rb +3 -1
- data/lib/insta.rb +42 -8
- data/lib/minitest/insta_plugin.rb +17 -0
- data/sig/insta/ansi.rbs +28 -0
- data/sig/insta/ci.rbs +10 -0
- data/sig/insta/cli.rbs +150 -0
- data/sig/insta/config_manifest.rbs +27 -0
- data/sig/insta/configuration.rbs +48 -0
- data/sig/insta/diff.rbs +38 -0
- data/sig/insta/inline/call_finder.rbs +22 -0
- data/sig/insta/inline/edit.rbs +18 -0
- data/sig/insta/inline/file_patcher.rbs +55 -0
- data/sig/insta/inline/pending_registry.rbs +22 -0
- data/sig/insta/inline/pending_store.rbs +41 -0
- data/sig/insta/minitest/assertions.rbs +52 -0
- data/sig/insta/minitest/lifecycle.rbs +10 -0
- data/sig/insta/minitest.rbs +2 -0
- data/sig/insta/pending_locations.rbs +22 -0
- data/sig/insta/pending_reporter.rbs +14 -0
- data/sig/insta/redaction/applicator.rbs +22 -0
- data/sig/insta/redaction/selector.rbs +58 -0
- data/sig/insta/rspec/hooks.rbs +10 -0
- data/sig/insta/rspec/matchers.rbs +71 -0
- data/sig/insta/rspec.rbs +2 -0
- data/sig/insta/serializers/base.rbs +21 -0
- data/sig/insta/serializers/inspect.rbs +10 -0
- data/sig/insta/serializers/json.rbs +10 -0
- data/sig/insta/serializers/string.rbs +10 -0
- data/sig/insta/serializers/yaml.rbs +10 -0
- data/sig/insta/snapshot.rbs +37 -0
- data/sig/insta/snapshot_content.rbs +14 -0
- data/sig/insta/snapshot_file.rbs +27 -0
- data/sig/insta/snapshot_mismatch_handler.rbs +12 -0
- data/sig/insta/snapshot_name.rbs +17 -0
- data/sig/insta/syntax_highlight.rbs +8 -0
- data/sig/insta/update_coordinator.rbs +11 -0
- data/sig/insta/version.rbs +5 -0
- data/sig/insta.rbs +12 -0
- data/sig/minitest/insta_plugin.rbs +6 -0
- data/sig/vendor/difftastic.rbs +19 -0
- data/sig/vendor/irb.rbs +5 -0
- data/sig/vendor/psych.rbs +7 -0
- data/sig/vendor/rspec.rbs +9 -0
- data/sig/vendor/rubygems.rbs +3 -0
- metadata +109 -54
- data/.DS_Store +0 -0
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.travis.yml +0 -5
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -35
- data/README.md +0 -41
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/insta.gemspec +0 -36
- data/lib/.DS_Store +0 -0
- data/lib/insta/account.rb +0 -127
- data/lib/insta/api.rb +0 -88
- data/lib/insta/constants.rb +0 -21
- data/lib/insta/device.rb +0 -11852
- data/lib/insta/feed.rb +0 -89
- data/lib/insta/location.rb +0 -18
- data/lib/insta/proxy_manager.rb +0 -23
- data/lib/insta/tag.rb +0 -18
- data/lib/insta/user.rb +0 -103
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Insta
|
|
7
|
+
module Inline
|
|
8
|
+
# @rbs!
|
|
9
|
+
# type pending_store_entry = { file: String, line: Integer, content: String, old_content: String, type: String }
|
|
10
|
+
|
|
11
|
+
module PendingStore
|
|
12
|
+
FILENAME = ".insta-pending-inline"
|
|
13
|
+
|
|
14
|
+
@pending = [] #: Array[Inline::pending_store_entry]
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
#: (file: String, line: Integer, content: String, old_content: String, type: Symbol) -> void
|
|
18
|
+
def self.add(file:, line:, content:, old_content:, type:)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
@pending << {
|
|
21
|
+
file: file,
|
|
22
|
+
line: line,
|
|
23
|
+
content: content,
|
|
24
|
+
old_content: old_content,
|
|
25
|
+
type: type.to_s,
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: () -> void
|
|
31
|
+
def self.flush!
|
|
32
|
+
entries = @mutex.synchronize do
|
|
33
|
+
result = @pending.dup
|
|
34
|
+
@pending.clear
|
|
35
|
+
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return if entries.empty?
|
|
40
|
+
|
|
41
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
42
|
+
existing = if File.exist?(manifest_path)
|
|
43
|
+
JSON.parse(File.read(manifest_path), symbolize_names: true)
|
|
44
|
+
else
|
|
45
|
+
[] #: Array[Inline::pending_store_entry]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
merged = merge_entries(existing, entries)
|
|
49
|
+
|
|
50
|
+
FileUtils.mkdir_p(File.dirname(manifest_path))
|
|
51
|
+
File.write(manifest_path, JSON.pretty_generate(merged))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#: () -> Array[Inline::pending_store_entry]
|
|
55
|
+
def self.load
|
|
56
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
57
|
+
return [] unless File.exist?(manifest_path)
|
|
58
|
+
|
|
59
|
+
JSON.parse(File.read(manifest_path), symbolize_names: true)
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: () -> void
|
|
65
|
+
def self.clean!
|
|
66
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
67
|
+
|
|
68
|
+
FileUtils.rm_f(manifest_path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: (Array[Inline::pending_store_entry]) -> void
|
|
72
|
+
def self.apply!(entries)
|
|
73
|
+
grouped = entries.group_by { |entry| entry[:file] }
|
|
74
|
+
|
|
75
|
+
grouped.each do |file, file_entries|
|
|
76
|
+
next unless File.exist?(file)
|
|
77
|
+
|
|
78
|
+
pending = file_entries.map { |entry|
|
|
79
|
+
{ line: entry[:line], content: entry[:content], type: entry[:type].to_sym }
|
|
80
|
+
} #: Array[Inline::pending_entry]
|
|
81
|
+
|
|
82
|
+
patched = FilePatcher.patch(file, pending)
|
|
83
|
+
|
|
84
|
+
FilePatcher.atomic_write(file, patched)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#: (Array[Inline::pending_store_entry]) -> void
|
|
89
|
+
def self.remove!(entries)
|
|
90
|
+
current = self.load
|
|
91
|
+
return if current.empty?
|
|
92
|
+
|
|
93
|
+
remaining = current.reject { |existing|
|
|
94
|
+
entries.any? { |entry|
|
|
95
|
+
existing[:file] == entry[:file] && existing[:line] == entry[:line]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
100
|
+
|
|
101
|
+
if remaining.empty?
|
|
102
|
+
FileUtils.rm_f(manifest_path)
|
|
103
|
+
else
|
|
104
|
+
File.write(manifest_path, JSON.pretty_generate(remaining))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#: () -> bool
|
|
109
|
+
def self.any?
|
|
110
|
+
@mutex.synchronize { !@pending.empty? }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#: () -> Integer
|
|
114
|
+
def self.size
|
|
115
|
+
@mutex.synchronize { @pending.length }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#: () -> void
|
|
119
|
+
def self.clear!
|
|
120
|
+
@mutex.synchronize { @pending.clear }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (Array[Inline::pending_store_entry], Array[Inline::pending_store_entry]) -> Array[Inline::pending_store_entry]
|
|
124
|
+
def self.merge_entries(existing, new_entries)
|
|
125
|
+
result = existing.dup
|
|
126
|
+
|
|
127
|
+
new_entries.each do |entry|
|
|
128
|
+
index = result.index { |e|
|
|
129
|
+
e[:file] == entry[:file] && e[:line] == entry[:line]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if index
|
|
133
|
+
result[index] = entry
|
|
134
|
+
else
|
|
135
|
+
result << entry
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
result
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private_class_method :merge_entries
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insta
|
|
4
|
+
module Minitest
|
|
5
|
+
# @rbs module-self: Minitest::Test
|
|
6
|
+
module Assertions
|
|
7
|
+
#: (
|
|
8
|
+
#| untyped,
|
|
9
|
+
#| ?name: String?,
|
|
10
|
+
#| ?serializer: Symbol?,
|
|
11
|
+
#| ?redact: Hash[String, untyped]?,
|
|
12
|
+
#| ?input: String?,
|
|
13
|
+
#| ?description: String?,
|
|
14
|
+
#| ?info: Hash[String, untyped]?,
|
|
15
|
+
#| ?options: Hash[Symbol, untyped]?
|
|
16
|
+
#| ) -> void
|
|
17
|
+
def assert_snapshot(actual, name: nil, serializer: nil, redact: nil, input: nil, description: nil, info: nil, options: nil)
|
|
18
|
+
expression = actual.class.name
|
|
19
|
+
serializer_name = serializer || Insta.configuration.default_serializer
|
|
20
|
+
actual = Redaction::Applicator.apply(actual, redact) if redact
|
|
21
|
+
serialized = Serializers.serialize(serializer_name, actual)
|
|
22
|
+
content = SnapshotContent.normalize(serialized)
|
|
23
|
+
|
|
24
|
+
test_class = self.class.name || "Anonymous"
|
|
25
|
+
test_method = self.name || "unknown"
|
|
26
|
+
|
|
27
|
+
path = if name
|
|
28
|
+
snapshot_file = SnapshotFile.new(
|
|
29
|
+
Insta.configuration.snapshot_path,
|
|
30
|
+
test_class,
|
|
31
|
+
test_method
|
|
32
|
+
)
|
|
33
|
+
snapshot_file.named_path(name)
|
|
34
|
+
else
|
|
35
|
+
snapshot_file = _insta_snapshot_file
|
|
36
|
+
snapshot_file.path_for(options || {})
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
metadata = build_metadata(
|
|
40
|
+
test_class,
|
|
41
|
+
test_method,
|
|
42
|
+
expression: expression,
|
|
43
|
+
input: input,
|
|
44
|
+
description: description,
|
|
45
|
+
info: info,
|
|
46
|
+
options: options
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
existing = snapshot_file.read(path)
|
|
50
|
+
|
|
51
|
+
unless existing
|
|
52
|
+
handle_missing_snapshot(snapshot_file, path, content, metadata)
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if existing.expression && existing.expression != expression
|
|
57
|
+
caller_line = find_test_caller
|
|
58
|
+
handle_type_mismatch(snapshot_file, path, content, metadata, existing, expression, caller_line)
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
expected = SnapshotContent.normalize(existing.content)
|
|
63
|
+
|
|
64
|
+
if content == expected
|
|
65
|
+
pass
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
caller_line = find_test_caller
|
|
70
|
+
handle_snapshot_mismatch(snapshot_file, path, content, metadata, expected, test_class, test_method, caller_line)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#: (
|
|
74
|
+
#| untyped,
|
|
75
|
+
#| ?String?,
|
|
76
|
+
#| ?serializer: Symbol?,
|
|
77
|
+
#| ?redact: Hash[String, untyped]?
|
|
78
|
+
#| ) -> void
|
|
79
|
+
def assert_inline_snapshot(actual, expected = nil, serializer: nil, redact: nil)
|
|
80
|
+
serializer_name = serializer || Insta.configuration.default_serializer
|
|
81
|
+
actual = Redaction::Applicator.apply(actual, redact) if redact
|
|
82
|
+
serialized = Serializers.serialize(serializer_name, actual)
|
|
83
|
+
content = SnapshotContent.normalize(serialized)
|
|
84
|
+
|
|
85
|
+
location = caller_locations(1, 1)&.first
|
|
86
|
+
return flunk("Could not determine caller location") unless location
|
|
87
|
+
|
|
88
|
+
file = location.path
|
|
89
|
+
line = location.lineno
|
|
90
|
+
|
|
91
|
+
return flunk("Could not determine source file") unless file
|
|
92
|
+
|
|
93
|
+
if expected.nil?
|
|
94
|
+
if Insta.configuration.new_snapshot == :review
|
|
95
|
+
Inline::PendingStore.add(file: file, line: line, content: content, old_content: "", type: :insert)
|
|
96
|
+
|
|
97
|
+
if ENV["INSTA_FORCE_PASS"]
|
|
98
|
+
pass
|
|
99
|
+
else
|
|
100
|
+
flunk "New inline snapshot pending review at #{file}:#{line}\nRun `bundle exec insta review` to accept."
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
Inline::PendingRegistry.add(file: file, line: line, content: content, type: :insert)
|
|
104
|
+
pass
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
expected_normalized = SnapshotContent.normalize(expected.to_s)
|
|
111
|
+
|
|
112
|
+
if content == expected_normalized
|
|
113
|
+
pass
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
handle_inline_mismatch(file, line, content, expected_normalized)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
#: () -> SnapshotFile
|
|
123
|
+
def _insta_snapshot_file
|
|
124
|
+
@_insta_snapshot_file ||= SnapshotFile.new(
|
|
125
|
+
Insta.configuration.snapshot_path,
|
|
126
|
+
self.class.name || "Anonymous",
|
|
127
|
+
name || "unknown"
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
#: () -> String
|
|
132
|
+
def find_test_caller
|
|
133
|
+
locations = caller_locations
|
|
134
|
+
return locations&.first.to_s unless locations
|
|
135
|
+
|
|
136
|
+
test_location = locations.find { |location|
|
|
137
|
+
path = location.path
|
|
138
|
+
|
|
139
|
+
path && !path.include?("lib/insta") && path.match?(/_test\.rb\z/)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
(test_location || locations.first).to_s
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#: (String, String, ?expression: String?, ?input: String?, ?description: String?,
|
|
146
|
+
#| ?info: Hash[String, untyped]?, ?options: Hash[Symbol, untyped]?) -> Snapshot::snapshot_metadata
|
|
147
|
+
def build_metadata(test_class, test_method, expression: nil, input: nil, description: nil, info: nil, options: nil)
|
|
148
|
+
metadata = { "source" => "#{test_class}##{test_method}" }
|
|
149
|
+
|
|
150
|
+
metadata["expression"] = expression if expression
|
|
151
|
+
metadata["input"] = input if input
|
|
152
|
+
metadata["description"] = description if description
|
|
153
|
+
metadata["options"] = options if options && !options.empty?
|
|
154
|
+
metadata["info"] = info if info
|
|
155
|
+
|
|
156
|
+
metadata
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
#: (SnapshotFile, Pathname, String, Snapshot::snapshot_metadata) -> void
|
|
160
|
+
def handle_missing_snapshot(snapshot_file, path, content, metadata)
|
|
161
|
+
mode = Insta.configuration.resolved_update_mode
|
|
162
|
+
|
|
163
|
+
if mode == :no
|
|
164
|
+
flunk "Snapshot file does not exist: #{path}\nRun with INSTA_UPDATE=force to create it."
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if Insta.configuration.new_snapshot == :review
|
|
169
|
+
pending_path = snapshot_file.pending_path(path)
|
|
170
|
+
snapshot_file.write(pending_path, content, metadata)
|
|
171
|
+
PendingLocations.add(pending_path.to_s, find_test_caller)
|
|
172
|
+
|
|
173
|
+
if ENV["INSTA_FORCE_PASS"]
|
|
174
|
+
pass
|
|
175
|
+
else
|
|
176
|
+
flunk "New snapshot pending review: #{path}\nRun `bundle exec insta review` to accept."
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
snapshot_file.write(path, content, metadata)
|
|
183
|
+
|
|
184
|
+
pass
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
#: (SnapshotFile, Pathname, String, Snapshot::snapshot_metadata, Snapshot, String?, String) -> void
|
|
188
|
+
def handle_type_mismatch(snapshot_file, path, content, metadata, existing, expression, caller_line)
|
|
189
|
+
expected = SnapshotContent.normalize(existing.content)
|
|
190
|
+
result = SnapshotMismatchHandler.handle(snapshot_file, path, content, metadata, expected, caller_line: caller_line)
|
|
191
|
+
|
|
192
|
+
case result
|
|
193
|
+
when :updated, :force_passed
|
|
194
|
+
pass
|
|
195
|
+
else
|
|
196
|
+
flunk "Snapshot type mismatch: snapshot was #{existing.expression} but got #{expression}\n" \
|
|
197
|
+
"Run `bundle exec insta review` or delete the snapshot file and re-run the test to update it."
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
#: (SnapshotFile, Pathname, String, Snapshot::snapshot_metadata, String, String, String, String) -> void
|
|
202
|
+
def handle_snapshot_mismatch(snapshot_file, path, content, metadata, expected, test_class, test_method, caller_line)
|
|
203
|
+
result = SnapshotMismatchHandler.handle(snapshot_file, path, content, metadata, expected, caller_line: caller_line)
|
|
204
|
+
|
|
205
|
+
case result
|
|
206
|
+
when :updated, :force_passed
|
|
207
|
+
pass
|
|
208
|
+
else
|
|
209
|
+
label = "#{test_class}##{test_method}"
|
|
210
|
+
flunk SnapshotMismatchHandler.failure_message(expected, content, label, path, caller_line: caller_line)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
#: (String, Integer, String, String) -> void
|
|
215
|
+
def handle_inline_mismatch(file, line, content, expected_normalized)
|
|
216
|
+
mode = Insta.configuration.resolved_update_mode
|
|
217
|
+
|
|
218
|
+
case mode
|
|
219
|
+
when :force
|
|
220
|
+
Inline::PendingRegistry.add(file: file, line: line, content: content, type: :replace)
|
|
221
|
+
pass
|
|
222
|
+
when :no
|
|
223
|
+
flunk Diff.failure_message(
|
|
224
|
+
expected_normalized,
|
|
225
|
+
content,
|
|
226
|
+
"inline snapshot at #{file}:#{line}",
|
|
227
|
+
nil,
|
|
228
|
+
"#{file}:#{line}"
|
|
229
|
+
)
|
|
230
|
+
else # :auto, :pending
|
|
231
|
+
Inline::PendingStore.add(
|
|
232
|
+
file: file,
|
|
233
|
+
line: line,
|
|
234
|
+
content: content,
|
|
235
|
+
old_content: expected_normalized,
|
|
236
|
+
type: :replace
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if ENV["INSTA_FORCE_PASS"]
|
|
240
|
+
pass
|
|
241
|
+
else
|
|
242
|
+
flunk Diff.failure_message(
|
|
243
|
+
expected_normalized,
|
|
244
|
+
content,
|
|
245
|
+
"inline snapshot at #{file}:#{line}",
|
|
246
|
+
nil,
|
|
247
|
+
"#{file}:#{line}"
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Insta
|
|
6
|
+
module PendingLocations
|
|
7
|
+
FILENAME = ".insta-pending-locations"
|
|
8
|
+
|
|
9
|
+
@locations = {} #: Hash[String, String]
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
|
|
12
|
+
#: (String, String) -> void
|
|
13
|
+
def self.add(path, caller_line)
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
@locations[path] = caller_line
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: () -> void
|
|
20
|
+
def self.flush!
|
|
21
|
+
entries = @mutex.synchronize do
|
|
22
|
+
result = @locations.dup
|
|
23
|
+
@locations.clear
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return if entries.empty?
|
|
28
|
+
|
|
29
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
30
|
+
existing = if File.exist?(manifest_path)
|
|
31
|
+
JSON.parse(File.read(manifest_path))
|
|
32
|
+
else
|
|
33
|
+
{} #: Hash[String, String]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
existing.merge!(entries)
|
|
37
|
+
|
|
38
|
+
FileUtils.mkdir_p(File.dirname(manifest_path))
|
|
39
|
+
File.write(manifest_path, JSON.pretty_generate(existing))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#: () -> Hash[String, String]
|
|
43
|
+
def self.load
|
|
44
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
45
|
+
return {} unless File.exist?(manifest_path)
|
|
46
|
+
|
|
47
|
+
JSON.parse(File.read(manifest_path))
|
|
48
|
+
rescue JSON::ParserError
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#: () -> void
|
|
53
|
+
def self.clean!
|
|
54
|
+
manifest_path = File.join(Insta.configuration.snapshot_path, FILENAME)
|
|
55
|
+
|
|
56
|
+
FileUtils.rm_f(manifest_path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#: () -> void
|
|
60
|
+
def self.clear!
|
|
61
|
+
@mutex.synchronize { @locations.clear }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insta
|
|
4
|
+
module PendingReporter
|
|
5
|
+
#: () -> void
|
|
6
|
+
def self.flush_and_report!
|
|
7
|
+
Inline::PendingRegistry.flush!
|
|
8
|
+
Inline::PendingStore.flush!
|
|
9
|
+
PendingLocations.flush!
|
|
10
|
+
ConfigManifest.write!
|
|
11
|
+
|
|
12
|
+
report_pending_files!
|
|
13
|
+
report_pending_inline!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#: () -> void
|
|
17
|
+
def self.report_pending_files!
|
|
18
|
+
snapshot_path = Insta.configuration.snapshot_path
|
|
19
|
+
extension = Insta.configuration.snapshot_extension
|
|
20
|
+
pending_files = Dir.glob(File.join(snapshot_path, "**", "*#{extension}.new"))
|
|
21
|
+
|
|
22
|
+
return unless pending_files.any?
|
|
23
|
+
|
|
24
|
+
count = pending_files.length
|
|
25
|
+
noun = count == 1 ? "snapshot" : "snapshots"
|
|
26
|
+
warn "\n\e[33m●\e[0m #{count} pending file #{noun}:\n\n"
|
|
27
|
+
pending_files.each { |f| warn " \e[33m›\e[0m #{f}" }
|
|
28
|
+
warn "\n \e[36mbundle exec insta review\e[0m\n\n"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: () -> void
|
|
32
|
+
def self.report_pending_inline!
|
|
33
|
+
entries = Inline::PendingStore.load
|
|
34
|
+
return unless entries.any?
|
|
35
|
+
|
|
36
|
+
count = entries.length
|
|
37
|
+
noun = count == 1 ? "snapshot" : "snapshots"
|
|
38
|
+
warn "\n\e[33m●\e[0m #{count} pending inline #{noun}:\n\n"
|
|
39
|
+
|
|
40
|
+
entries.each do |entry|
|
|
41
|
+
warn " \e[33m›\e[0m #{entry[:file]}:#{entry[:line]}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
warn "\n \e[36mbundle exec insta review\e[0m\n\n"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insta
|
|
4
|
+
module Redaction
|
|
5
|
+
class Applicator
|
|
6
|
+
#: (untyped, Hash[String, untyped]?) -> untyped
|
|
7
|
+
def self.apply(value, redactions)
|
|
8
|
+
return value if redactions.nil? || redactions.empty?
|
|
9
|
+
|
|
10
|
+
unless value.is_a?(Hash) || value.is_a?(Array)
|
|
11
|
+
raise ArgumentError,
|
|
12
|
+
"Redactions require structured data (Hash or Array), got #{value.class}. " \
|
|
13
|
+
"Use a structured serializer like `serializer: :json` or `serializer: :yaml` with Hash/Array values."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
duped = deep_dup(value)
|
|
17
|
+
parsed = redactions.transform_keys { |key| Selector.new(key) }
|
|
18
|
+
|
|
19
|
+
walk(duped, [], parsed)
|
|
20
|
+
|
|
21
|
+
duped
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#: (untyped) -> untyped
|
|
25
|
+
def self.deep_dup(object)
|
|
26
|
+
case object
|
|
27
|
+
when Hash
|
|
28
|
+
hash = {} #: Hash[untyped, untyped]
|
|
29
|
+
object.each_with_object(hash) { |(key, value), result| result[key] = deep_dup(value) }
|
|
30
|
+
when Array
|
|
31
|
+
object.map { |value| deep_dup(value) }
|
|
32
|
+
else
|
|
33
|
+
object
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#: (untyped, untyped, Hash[Selector, untyped]) -> void
|
|
38
|
+
def self.walk(node, path, parsed_redactions)
|
|
39
|
+
case node
|
|
40
|
+
when Hash
|
|
41
|
+
node.each_key do |key|
|
|
42
|
+
child_path = path + [{ type: :key, value: key }]
|
|
43
|
+
replacement = find_match(child_path, parsed_redactions)
|
|
44
|
+
|
|
45
|
+
if replacement
|
|
46
|
+
node[key] = apply_replacement(node[key], replacement)
|
|
47
|
+
else
|
|
48
|
+
walk(node[key], child_path, parsed_redactions)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
when Array
|
|
52
|
+
node.each_with_index do |_item, index|
|
|
53
|
+
child_path = path + [{ type: :index, value: index }]
|
|
54
|
+
replacement = find_match(child_path, parsed_redactions)
|
|
55
|
+
|
|
56
|
+
if replacement
|
|
57
|
+
node[index] = apply_replacement(node[index], replacement)
|
|
58
|
+
else
|
|
59
|
+
walk(node[index], child_path, parsed_redactions)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: (untyped, Hash[Selector, untyped]) -> untyped
|
|
66
|
+
def self.find_match(path, parsed_redactions)
|
|
67
|
+
parsed_redactions.each do |selector, replacement|
|
|
68
|
+
return replacement if selector.matches?(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#: (untyped, untyped) -> untyped
|
|
75
|
+
def self.apply_replacement(value, replacement)
|
|
76
|
+
case replacement
|
|
77
|
+
when Proc
|
|
78
|
+
replacement.call(value)
|
|
79
|
+
when :sorted
|
|
80
|
+
value.is_a?(Array) ? value.sort_by(&:to_s) : value
|
|
81
|
+
else
|
|
82
|
+
replacement
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method :deep_dup, :walk, :find_match, :apply_replacement
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|