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
data/lib/insta/cli.rb
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../insta"
|
|
4
|
+
require_relative "ansi"
|
|
5
|
+
require_relative "syntax_highlight"
|
|
6
|
+
|
|
7
|
+
module Insta
|
|
8
|
+
class CLI
|
|
9
|
+
include ANSI
|
|
10
|
+
|
|
11
|
+
COMMANDS = ["review", "accept", "reject", "pending", "status", "clean", "help"].freeze
|
|
12
|
+
MIN_GAP = 8 #: Integer
|
|
13
|
+
|
|
14
|
+
#: (Array[String]) -> void
|
|
15
|
+
def initialize(args)
|
|
16
|
+
@command = args.first || "help"
|
|
17
|
+
@args = args.drop(1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: () -> void
|
|
21
|
+
def run
|
|
22
|
+
ConfigManifest.apply!
|
|
23
|
+
|
|
24
|
+
case @command
|
|
25
|
+
when "review" then review
|
|
26
|
+
when "accept" then accept
|
|
27
|
+
when "reject" then reject
|
|
28
|
+
when "pending" then pending_list
|
|
29
|
+
when "status" then status
|
|
30
|
+
when "clean" then clean
|
|
31
|
+
when "help", "--help", "-h" then help
|
|
32
|
+
when "--version", "-v" then version
|
|
33
|
+
else
|
|
34
|
+
warn header
|
|
35
|
+
warn
|
|
36
|
+
warn "#{dim("Invalid command:")} #{red(@command)}"
|
|
37
|
+
warn
|
|
38
|
+
warn "Run #{cyan("insta help")} for usage information."
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def pluralize(count, singular, plural = "#{singular}s") = count == 1 ? singular : plural
|
|
46
|
+
|
|
47
|
+
#: () -> void
|
|
48
|
+
def review
|
|
49
|
+
pending_files = find_pending_files
|
|
50
|
+
inline_entries = Inline::PendingStore.load
|
|
51
|
+
|
|
52
|
+
if pending_files.empty? && inline_entries.empty?
|
|
53
|
+
puts "#{green("✓")} No pending snapshots to review."
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@pending_locations = PendingLocations.load
|
|
58
|
+
@accepted_count = 0
|
|
59
|
+
@rejected_count = 0
|
|
60
|
+
|
|
61
|
+
total = pending_files.length + inline_entries.length
|
|
62
|
+
puts "#{yellow("●")} Found #{bold(total.to_s)} pending #{pluralize(total, "snapshot")} to review.\n\n"
|
|
63
|
+
|
|
64
|
+
@quit = false
|
|
65
|
+
review_files(pending_files) unless pending_files.empty?
|
|
66
|
+
review_inline_entries(inline_entries) unless @quit || inline_entries.empty?
|
|
67
|
+
|
|
68
|
+
print_review_summary
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: (Array[String]) -> void
|
|
72
|
+
def review_files(files)
|
|
73
|
+
accept_all = false
|
|
74
|
+
reject_all = false
|
|
75
|
+
|
|
76
|
+
files.each do |pending_file|
|
|
77
|
+
if accept_all
|
|
78
|
+
accept_file(pending_file)
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if reject_all
|
|
83
|
+
reject_file(pending_file)
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
result = review_single_file(pending_file)
|
|
88
|
+
|
|
89
|
+
case result
|
|
90
|
+
when :accept_all
|
|
91
|
+
accept_all = true
|
|
92
|
+
when :reject_all
|
|
93
|
+
reject_all = true
|
|
94
|
+
when :quit
|
|
95
|
+
puts "Quit."
|
|
96
|
+
@quit = true
|
|
97
|
+
break
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
#: (String) -> Symbol
|
|
103
|
+
def review_single_file(pending_file)
|
|
104
|
+
original = pending_file.sub(/\.new\z/, "")
|
|
105
|
+
snap_name = File.basename(original)
|
|
106
|
+
|
|
107
|
+
puts bold("─── #{snap_name} ───")
|
|
108
|
+
display_source_context(pending_file)
|
|
109
|
+
display_snapshot_paths(original, pending_file)
|
|
110
|
+
puts
|
|
111
|
+
display_diff(original, pending_file)
|
|
112
|
+
|
|
113
|
+
response = prompt_review_action
|
|
114
|
+
process_review_response(response, pending_file)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#: () -> String
|
|
118
|
+
def prompt_review_action
|
|
119
|
+
puts
|
|
120
|
+
print review_prompt_line
|
|
121
|
+
|
|
122
|
+
response = $stdin.gets&.strip || "s"
|
|
123
|
+
|
|
124
|
+
if response.downcase == "h"
|
|
125
|
+
print_review_help
|
|
126
|
+
print review_prompt_line
|
|
127
|
+
response = $stdin.gets&.strip || "s"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
response.downcase
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
#: () -> String
|
|
134
|
+
def review_prompt_line
|
|
135
|
+
" #{bold(green("[a]ccept"))} #{bold(red("[r]eject"))} #{dim("[s]kip")} " \
|
|
136
|
+
"#{bold(green("[A]ccept all"))} #{bold(red("[R]eject all"))} " \
|
|
137
|
+
"#{dim("[q]uit")} #{dim("[h]elp")}: "
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: (String) -> void
|
|
141
|
+
def display_source_context(pending_file)
|
|
142
|
+
locations = @pending_locations || {}
|
|
143
|
+
caller_location = locations[pending_file]
|
|
144
|
+
|
|
145
|
+
if caller_location
|
|
146
|
+
display_caller_context(caller_location)
|
|
147
|
+
else
|
|
148
|
+
snapshot = Snapshot.parse(File.read(pending_file))
|
|
149
|
+
source = snapshot.source
|
|
150
|
+
puts " #{dim("Source:")} #{cyan(source)}" if source
|
|
151
|
+
|
|
152
|
+
test_file = find_test_file(pending_file)
|
|
153
|
+
return unless test_file
|
|
154
|
+
|
|
155
|
+
method_name = source&.split("#")&.last
|
|
156
|
+
lineno = method_name ? find_test_line(test_file, method_name) : nil
|
|
157
|
+
location = lineno ? "#{test_file}:#{lineno}" : test_file
|
|
158
|
+
|
|
159
|
+
puts " #{dim("Test:")} #{cyan(location)}"
|
|
160
|
+
|
|
161
|
+
print_source_lines(test_file, lineno) if lineno
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#: (String) -> String?
|
|
166
|
+
def find_test_file(pending_file)
|
|
167
|
+
snapshot_path = Insta.configuration.snapshot_path
|
|
168
|
+
relative = pending_file.sub(%r{\A#{Regexp.escape(snapshot_path)}/}, "")
|
|
169
|
+
test_dir = File.dirname(relative)
|
|
170
|
+
|
|
171
|
+
test_file = File.join("test", "#{test_dir}.rb")
|
|
172
|
+
return test_file if File.exist?(test_file)
|
|
173
|
+
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
#: (String, String) -> Integer?
|
|
178
|
+
def find_test_line(test_file, method_name)
|
|
179
|
+
lines = File.readlines(test_file)
|
|
180
|
+
|
|
181
|
+
lines.each_with_index do |line, i|
|
|
182
|
+
return i + 1 if line.match?(/\bdef\s+#{Regexp.escape(method_name)}\b/)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
description = method_name.sub(/\Atest_\d+_/, "").tr("_", " ")
|
|
186
|
+
|
|
187
|
+
lines.each_with_index do |line, i|
|
|
188
|
+
return i + 1 if line.include?("test \"#{description}\"") || line.include?("test '#{description}'")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#: (String) -> void
|
|
195
|
+
def display_caller_context(caller_location)
|
|
196
|
+
file, lineno_string = caller_location.split(":", 3).first(2)
|
|
197
|
+
return unless file && lineno_string
|
|
198
|
+
|
|
199
|
+
lineno = lineno_string.to_i
|
|
200
|
+
return unless lineno.positive? && File.exist?(file)
|
|
201
|
+
|
|
202
|
+
absolute_path = File.expand_path(file)
|
|
203
|
+
puts " #{dim("Source:")} #{cyan("#{absolute_path}:#{lineno}")}"
|
|
204
|
+
|
|
205
|
+
print_source_lines(file, lineno)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
#: (String, String) -> void
|
|
209
|
+
def display_snapshot_paths(original, pending_file)
|
|
210
|
+
puts " #{dim("Old:")} #{dim(original)}"
|
|
211
|
+
puts " #{dim("New:")} #{dim(pending_file)}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
#: () -> void
|
|
215
|
+
def print_review_help
|
|
216
|
+
extension = Insta.configuration.snapshot_extension
|
|
217
|
+
|
|
218
|
+
puts
|
|
219
|
+
puts " #{bold(green("a"))} #{bold(green("accept"))} " \
|
|
220
|
+
"#{dim("Apply the new snapshot as the current #{extension} file")}"
|
|
221
|
+
puts " #{bold(red("r"))} #{bold(red("reject"))} " \
|
|
222
|
+
"#{dim("Discard the #{extension}.new file and keep the current snapshot")}"
|
|
223
|
+
puts " #{dim("s")} #{dim("skip")} " \
|
|
224
|
+
"#{dim("Skip this snapshot and continue reviewing")}"
|
|
225
|
+
puts " #{bold(green("A"))} #{bold(green("accept all"))} " \
|
|
226
|
+
"#{dim("Accept this and all remaining pending snapshots")}"
|
|
227
|
+
puts " #{bold(red("R"))} #{bold(red("reject all"))} " \
|
|
228
|
+
"#{dim("Reject this and all remaining pending snapshots")}"
|
|
229
|
+
puts " #{dim("q")} #{dim("quit")} " \
|
|
230
|
+
"#{dim("Stop reviewing and exit")}"
|
|
231
|
+
puts
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#: (String, Integer) -> void
|
|
235
|
+
def print_source_lines(file, lineno)
|
|
236
|
+
lines = File.readlines(file)
|
|
237
|
+
start_line = [lineno - 3, 0].max
|
|
238
|
+
end_line = [lineno + 1, lines.length - 1].min
|
|
239
|
+
|
|
240
|
+
highlighted = highlight_lines(lines, start_line, end_line)
|
|
241
|
+
puts
|
|
242
|
+
|
|
243
|
+
(start_line..end_line).each do |i|
|
|
244
|
+
line_num = i + 1
|
|
245
|
+
prefix = line_num == lineno ? cyan("→ ") : " "
|
|
246
|
+
|
|
247
|
+
puts " #{prefix}#{dim(format("%4d", line_num))} #{highlighted[i - start_line]}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
puts
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
#: (Array[String], Integer, Integer) -> Array[String]
|
|
254
|
+
def highlight_lines(lines, start_line, end_line)
|
|
255
|
+
snippet = lines[start_line..end_line] || []
|
|
256
|
+
code = snippet.join
|
|
257
|
+
|
|
258
|
+
SyntaxHighlight.highlight(code, colorable: color?).lines
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
#: (String, String) -> void
|
|
262
|
+
def display_diff(original, pending_file)
|
|
263
|
+
if File.exist?(original)
|
|
264
|
+
old_content = File.read(original)
|
|
265
|
+
new_content = File.read(pending_file)
|
|
266
|
+
extension = File.extname(original).delete_prefix(".")
|
|
267
|
+
|
|
268
|
+
puts Diff.diff_with_language(old_content, new_content, file_extension: extension)
|
|
269
|
+
else
|
|
270
|
+
puts " #{dim("(new snapshot)")}"
|
|
271
|
+
puts File.read(pending_file)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
#: (String, String) -> Symbol
|
|
276
|
+
def process_review_response(response, pending_file)
|
|
277
|
+
case response
|
|
278
|
+
when "a"
|
|
279
|
+
accept_file(pending_file)
|
|
280
|
+
:continue
|
|
281
|
+
when "r"
|
|
282
|
+
reject_file(pending_file)
|
|
283
|
+
:continue
|
|
284
|
+
when "accept all"
|
|
285
|
+
accept_file(pending_file)
|
|
286
|
+
:accept_all
|
|
287
|
+
when "reject all"
|
|
288
|
+
reject_file(pending_file)
|
|
289
|
+
:reject_all
|
|
290
|
+
when "q"
|
|
291
|
+
:quit
|
|
292
|
+
else
|
|
293
|
+
puts " #{dim("Skipped.")}\n\n"
|
|
294
|
+
:continue
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
#: (Array[Inline::pending_store_entry]) -> void
|
|
299
|
+
def review_inline_entries(entries)
|
|
300
|
+
accept_all = false
|
|
301
|
+
reject_all = false
|
|
302
|
+
|
|
303
|
+
entries.each do |entry|
|
|
304
|
+
if accept_all
|
|
305
|
+
accept_inline_entry(entry)
|
|
306
|
+
next
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
if reject_all
|
|
310
|
+
reject_inline_entry(entry)
|
|
311
|
+
next
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
result = review_single_inline_entry(entry)
|
|
315
|
+
|
|
316
|
+
case result
|
|
317
|
+
when :accept_all
|
|
318
|
+
accept_all = true
|
|
319
|
+
when :reject_all
|
|
320
|
+
reject_all = true
|
|
321
|
+
when :quit
|
|
322
|
+
puts "Quit."
|
|
323
|
+
break
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
#: (Inline::pending_store_entry) -> Symbol
|
|
329
|
+
def review_single_inline_entry(entry)
|
|
330
|
+
file = entry[:file]
|
|
331
|
+
line = entry[:line]
|
|
332
|
+
|
|
333
|
+
puts bold("─── inline snapshot ───")
|
|
334
|
+
puts " #{dim("Source:")} #{cyan("#{File.expand_path(file)}:#{line}")}"
|
|
335
|
+
|
|
336
|
+
print_source_lines(file, line) if File.exist?(file)
|
|
337
|
+
|
|
338
|
+
old_content = entry[:old_content] || ""
|
|
339
|
+
new_content = entry[:content] || ""
|
|
340
|
+
|
|
341
|
+
puts Diff.diff(old_content, new_content)
|
|
342
|
+
|
|
343
|
+
response = prompt_review_action
|
|
344
|
+
process_inline_review_response(response, entry)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
#: (String, Inline::pending_store_entry) -> Symbol
|
|
348
|
+
def process_inline_review_response(response, entry)
|
|
349
|
+
case response
|
|
350
|
+
when "a"
|
|
351
|
+
accept_inline_entry(entry)
|
|
352
|
+
:continue
|
|
353
|
+
when "r"
|
|
354
|
+
reject_inline_entry(entry)
|
|
355
|
+
:continue
|
|
356
|
+
when "accept all"
|
|
357
|
+
accept_inline_entry(entry)
|
|
358
|
+
:accept_all
|
|
359
|
+
when "reject all"
|
|
360
|
+
reject_inline_entry(entry)
|
|
361
|
+
:reject_all
|
|
362
|
+
when "q"
|
|
363
|
+
:quit
|
|
364
|
+
else
|
|
365
|
+
puts " #{dim("Skipped.")}\n\n"
|
|
366
|
+
:continue
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
#: (Inline::pending_store_entry) -> void
|
|
371
|
+
def accept_inline_entry(entry)
|
|
372
|
+
file = entry[:file]
|
|
373
|
+
|
|
374
|
+
Inline::PendingStore.apply!([entry])
|
|
375
|
+
Inline::PendingStore.remove!([entry])
|
|
376
|
+
|
|
377
|
+
@accepted_count = (@accepted_count || 0) + 1
|
|
378
|
+
|
|
379
|
+
puts "\n #{green("✓")} #{File.basename(file)}:#{entry[:line]}"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
#: (Inline::pending_store_entry) -> void
|
|
383
|
+
def reject_inline_entry(entry)
|
|
384
|
+
file = entry[:file]
|
|
385
|
+
|
|
386
|
+
Inline::PendingStore.remove!([entry])
|
|
387
|
+
|
|
388
|
+
@rejected_count = (@rejected_count || 0) + 1
|
|
389
|
+
|
|
390
|
+
puts "\n #{red("✗")} #{File.basename(file)}:#{entry[:line]}"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
#: () -> void
|
|
394
|
+
def accept
|
|
395
|
+
pending_files = find_pending_files
|
|
396
|
+
inline_entries = Inline::PendingStore.load
|
|
397
|
+
count = pending_files.length + inline_entries.length
|
|
398
|
+
|
|
399
|
+
if count.zero?
|
|
400
|
+
puts "#{green("✓")} No pending snapshots to accept."
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
pending_files.each { |file| accept_file(file) }
|
|
405
|
+
|
|
406
|
+
unless inline_entries.empty?
|
|
407
|
+
Inline::PendingStore.apply!(inline_entries)
|
|
408
|
+
Inline::PendingStore.clean!
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
puts "\n#{green("✓")} Accepted #{bold(count.to_s)} #{pluralize(count, "snapshot")}."
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
#: () -> void
|
|
415
|
+
def reject
|
|
416
|
+
pending_files = find_pending_files
|
|
417
|
+
inline_entries = Inline::PendingStore.load
|
|
418
|
+
count = pending_files.length + inline_entries.length
|
|
419
|
+
|
|
420
|
+
if count.zero?
|
|
421
|
+
puts "#{green("✓")} No pending snapshots to reject."
|
|
422
|
+
return
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
pending_files.each { |file| reject_file(file) }
|
|
426
|
+
Inline::PendingStore.clean! unless inline_entries.empty?
|
|
427
|
+
|
|
428
|
+
puts "\n#{green("✓")} Rejected #{bold(count.to_s)} #{pluralize(count, "snapshot")}."
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
#: () -> void
|
|
432
|
+
def pending_list
|
|
433
|
+
pending_files = find_pending_files
|
|
434
|
+
inline_entries = Inline::PendingStore.load
|
|
435
|
+
|
|
436
|
+
if pending_files.empty? && inline_entries.empty?
|
|
437
|
+
puts "#{green("✓")} No pending snapshots."
|
|
438
|
+
return
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
total = pending_files.length + inline_entries.length
|
|
442
|
+
puts "#{yellow("●")} Pending snapshots #{dim("(#{total})")}:\n\n"
|
|
443
|
+
|
|
444
|
+
print_pending_files(pending_files) unless pending_files.empty?
|
|
445
|
+
|
|
446
|
+
return if inline_entries.empty?
|
|
447
|
+
|
|
448
|
+
puts unless pending_files.empty?
|
|
449
|
+
print_pending_inline(inline_entries)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
#: (Array[String]) -> void
|
|
453
|
+
def print_pending_files(pending_files)
|
|
454
|
+
locations = PendingLocations.load
|
|
455
|
+
|
|
456
|
+
pending_files.each do |pending_file|
|
|
457
|
+
caller_location = locations[pending_file]
|
|
458
|
+
|
|
459
|
+
if caller_location
|
|
460
|
+
file, lineno = caller_location.split(":", 3).first(2)
|
|
461
|
+
absolute_path = file ? File.expand_path(file) : nil
|
|
462
|
+
location = absolute_path ? "#{absolute_path}:#{lineno}" : caller_location
|
|
463
|
+
|
|
464
|
+
puts " #{yellow("›")} #{pending_file}"
|
|
465
|
+
puts " #{dim("at")} #{cyan(location)}"
|
|
466
|
+
else
|
|
467
|
+
puts " #{yellow("›")} #{pending_file}"
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
#: (Array[Inline::pending_store_entry]) -> void
|
|
473
|
+
def print_pending_inline(inline_entries)
|
|
474
|
+
inline_entries.each do |entry|
|
|
475
|
+
file = entry[:file]
|
|
476
|
+
line = entry[:line]
|
|
477
|
+
absolute_path = File.expand_path(file)
|
|
478
|
+
|
|
479
|
+
puts " #{yellow("›")} #{dim("inline")} #{File.basename(file)}:#{line}"
|
|
480
|
+
puts " #{dim("at")} #{cyan("#{absolute_path}:#{line}")}"
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
#: () -> void
|
|
485
|
+
def status
|
|
486
|
+
snapshot_path = Insta.configuration.snapshot_path
|
|
487
|
+
extension = Insta.configuration.snapshot_extension
|
|
488
|
+
|
|
489
|
+
puts header
|
|
490
|
+
puts
|
|
491
|
+
|
|
492
|
+
all_entries = Dir.exist?(snapshot_path) ? Dir.glob(File.join(snapshot_path, "**", "*")) : [] #: Array[String]
|
|
493
|
+
pending_extension = "#{extension}.new"
|
|
494
|
+
snap_files = all_entries.select { |file|
|
|
495
|
+
File.file?(file) && file.end_with?(extension) && !file.end_with?(pending_extension)
|
|
496
|
+
}
|
|
497
|
+
pending_files = all_entries.select { |file| File.file?(file) && file.end_with?(pending_extension) }
|
|
498
|
+
inline_entries = Inline::PendingStore.load
|
|
499
|
+
directories = all_entries.select { |file| File.directory?(file) }
|
|
500
|
+
|
|
501
|
+
print_status_config(snapshot_path, extension)
|
|
502
|
+
print_status_counts(snap_files, pending_files, inline_entries, directories)
|
|
503
|
+
print_status_pending(pending_files, inline_entries)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
#: (String, String) -> void
|
|
507
|
+
def print_status_config(snapshot_path, extension)
|
|
508
|
+
config_entries = [["Snapshot path", snapshot_path], ["Extension", extension]] #: Array[[String, String]]
|
|
509
|
+
print_status_entries(config_entries)
|
|
510
|
+
puts
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
#: (Array[String], Array[String], Array[Inline::pending_store_entry], Array[String]) -> void
|
|
514
|
+
def print_status_counts(snap_files, pending_files, inline_entries, directories)
|
|
515
|
+
print_status_entries(
|
|
516
|
+
[
|
|
517
|
+
["Snapshots", snap_files.length.to_s],
|
|
518
|
+
["Pending files", pending_files.empty? ? "0" : yellow(pending_files.length.to_s)],
|
|
519
|
+
["Pending inline", inline_entries.empty? ? "0" : yellow(inline_entries.length.to_s)],
|
|
520
|
+
["Directories", directories.length.to_s]
|
|
521
|
+
]
|
|
522
|
+
)
|
|
523
|
+
puts
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
#: (Array[String], Array[Inline::pending_store_entry]) -> void
|
|
527
|
+
def print_status_pending(pending_files, inline_entries)
|
|
528
|
+
if pending_files.empty? && inline_entries.empty?
|
|
529
|
+
puts "#{green("✓")} No pending snapshots."
|
|
530
|
+
return
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
unless pending_files.empty?
|
|
534
|
+
puts "Pending file snapshots:\n\n"
|
|
535
|
+
pending_files.each { |file| puts " #{yellow("›")} #{file}" }
|
|
536
|
+
puts
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
unless inline_entries.empty?
|
|
540
|
+
puts "Pending inline snapshots:\n\n"
|
|
541
|
+
inline_entries.each { |entry| puts " #{yellow("›")} #{entry[:file]}:#{entry[:line]}" }
|
|
542
|
+
puts
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
puts " #{cyan("bundle exec insta review")}\n"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
#: (Array[[String, String]]) -> void
|
|
549
|
+
def print_status_entries(entries)
|
|
550
|
+
max_label = entries.map { |label, _| label.length }.max || 0
|
|
551
|
+
|
|
552
|
+
entries.each do |label, value|
|
|
553
|
+
dot_count = MIN_GAP + (max_label - label.length)
|
|
554
|
+
dots = dim("·" * dot_count)
|
|
555
|
+
puts " #{cyan(label)} #{dots} #{value}"
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
#: () -> void
|
|
560
|
+
def clean
|
|
561
|
+
snapshot_path = Insta.configuration.snapshot_path
|
|
562
|
+
|
|
563
|
+
unless Dir.exist?(snapshot_path)
|
|
564
|
+
puts "#{red("✗")} Snapshot directory does not exist: #{bold(snapshot_path)}"
|
|
565
|
+
return
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
all_files = Dir.glob(File.join(snapshot_path, "**", "*"))
|
|
569
|
+
pending_files = all_files.select { |file| file.end_with?(".new") }
|
|
570
|
+
snap_files = all_files - pending_files
|
|
571
|
+
removed = 0
|
|
572
|
+
|
|
573
|
+
pending_files.each do |file|
|
|
574
|
+
File.delete(file)
|
|
575
|
+
removed += 1
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
inline_entries = Inline::PendingStore.load
|
|
579
|
+
removed += inline_entries.length
|
|
580
|
+
|
|
581
|
+
PendingLocations.clean!
|
|
582
|
+
Inline::PendingStore.clean!
|
|
583
|
+
|
|
584
|
+
puts "#{green("✓")} Removed #{bold(removed.to_s)} pending #{pluralize(removed, "snapshot")}." if removed.positive?
|
|
585
|
+
puts " #{dim("#{snap_files.length} snapshot file(s) in #{snapshot_path}")}"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
#: () -> void
|
|
589
|
+
def help
|
|
590
|
+
puts header
|
|
591
|
+
puts
|
|
592
|
+
puts dim("Description:")
|
|
593
|
+
puts " Snapshot tests assert values against a reference value. Think of it as"
|
|
594
|
+
puts " a supercharged version of #{cyan("assert_equal")} where the reference value is"
|
|
595
|
+
puts " managed by #{bold("insta")} for you."
|
|
596
|
+
puts
|
|
597
|
+
|
|
598
|
+
print_usage
|
|
599
|
+
print_section("Commands:", help_commands)
|
|
600
|
+
print_section("Options:", help_options)
|
|
601
|
+
print_section("Environment:", help_environment)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
#: () -> void
|
|
605
|
+
def print_usage
|
|
606
|
+
puts dim("Usage:")
|
|
607
|
+
puts " insta #{dim("<command>")} #{dim("[options]")}"
|
|
608
|
+
puts
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
#: (String, Array[[String, String]]) -> void
|
|
612
|
+
def print_section(title, entries)
|
|
613
|
+
puts dim(title)
|
|
614
|
+
|
|
615
|
+
max_label = entries.map { |label, _| label.length }.max || 0
|
|
616
|
+
|
|
617
|
+
entries.each do |label, description|
|
|
618
|
+
dot_count = MIN_GAP + (max_label - label.length)
|
|
619
|
+
dots = dim("·" * dot_count)
|
|
620
|
+
|
|
621
|
+
puts " #{cyan(label)} #{dots} #{description}"
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
puts
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
#: () -> String
|
|
628
|
+
def header
|
|
629
|
+
"#{bold("insta")} v#{Insta::VERSION} #{dim("📸 Snapshot Testing for Ruby")}"
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
#: () -> void
|
|
633
|
+
def version
|
|
634
|
+
puts header
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
#: () -> Array[[String, String]]
|
|
638
|
+
def help_commands
|
|
639
|
+
[
|
|
640
|
+
["review", "Interactive review of pending snapshots"],
|
|
641
|
+
["accept", "Accept all pending snapshots"],
|
|
642
|
+
["reject", "Reject all pending snapshots"],
|
|
643
|
+
["pending", "List pending snapshots"],
|
|
644
|
+
["status", "Show snapshot overview and pending files"],
|
|
645
|
+
["clean", "Remove pending snapshot files"],
|
|
646
|
+
["help", "Display usage information"]
|
|
647
|
+
]
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
#: () -> Array[[String, String]]
|
|
651
|
+
def help_options
|
|
652
|
+
[
|
|
653
|
+
["-h, --help", "Display usage information"],
|
|
654
|
+
["-v, --version", "Display version information"]
|
|
655
|
+
]
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
#: () -> Array[[String, String]]
|
|
659
|
+
def help_environment
|
|
660
|
+
[
|
|
661
|
+
["INSTA_UPDATE=<mode>", "Set update mode #{dim("[auto|always|new|no]")}"],
|
|
662
|
+
["INSTA_FORCE_PASS=1", "Create .snap.new without failing tests"]
|
|
663
|
+
]
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
#: () -> Array[String]
|
|
667
|
+
def find_pending_files
|
|
668
|
+
snapshot_path = Insta.configuration.snapshot_path
|
|
669
|
+
|
|
670
|
+
Dir.glob(File.join(snapshot_path, "**", "*.new"))
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
#: (String) -> void
|
|
674
|
+
def accept_file(pending_file)
|
|
675
|
+
original = pending_file.sub(/\.new\z/, "")
|
|
676
|
+
File.rename(pending_file, original)
|
|
677
|
+
|
|
678
|
+
@accepted_count = (@accepted_count || 0) + 1
|
|
679
|
+
|
|
680
|
+
puts "\n #{green("✓")} #{File.basename(original)}"
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
#: (String) -> void
|
|
684
|
+
def reject_file(pending_file)
|
|
685
|
+
File.delete(pending_file)
|
|
686
|
+
|
|
687
|
+
@rejected_count = (@rejected_count || 0) + 1
|
|
688
|
+
|
|
689
|
+
puts "\n #{red("✗")} #{File.basename(pending_file)}"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
#: () -> void
|
|
693
|
+
def print_review_summary
|
|
694
|
+
parts = [] #: Array[String]
|
|
695
|
+
parts << green("#{@accepted_count} accepted") if @accepted_count.positive?
|
|
696
|
+
parts << red("#{@rejected_count} rejected") if @rejected_count.positive?
|
|
697
|
+
|
|
698
|
+
remaining_files = find_pending_files
|
|
699
|
+
remaining_inline = Inline::PendingStore.load
|
|
700
|
+
remaining_count = remaining_files.length + remaining_inline.length
|
|
701
|
+
parts << yellow("#{remaining_count} skipped") if remaining_count.positive?
|
|
702
|
+
|
|
703
|
+
summary = parts.any? ? " #{dim("(")}#{parts.join(dim(", "))}#{dim(")")}" : ""
|
|
704
|
+
puts
|
|
705
|
+
|
|
706
|
+
if remaining_count.zero?
|
|
707
|
+
puts "#{green("✓")} Review complete.#{summary}"
|
|
708
|
+
else
|
|
709
|
+
puts "#{green("✓")} Review complete.#{summary}"
|
|
710
|
+
puts "\n #{cyan("bundle exec insta review")}\n\n"
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
end
|