snapshot 0.10.2 → 1.0.1

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.
@@ -0,0 +1,98 @@
1
+ require 'fastlane_core'
2
+
3
+ module Snapshot
4
+ class Options
5
+ def self.available_options
6
+ output_directory = (File.directory?("fastlane") ? "fastlane/screenshots" : "screenshots")
7
+
8
+ @@options ||= [
9
+ FastlaneCore::ConfigItem.new(key: :workspace,
10
+ short_option: "-w",
11
+ env_name: "SNAPSHOT_WORKSPACE",
12
+ optional: true,
13
+ description: "Path the workspace file",
14
+ verify_block: proc do |value|
15
+ v = File.expand_path(value.to_s)
16
+ raise "Workspace file not found at path '#{v}'".red unless File.exist?(v)
17
+ raise "Workspace file invalid".red unless File.directory?(v)
18
+ raise "Workspace file is not a workspace, must end with .xcworkspace".red unless v.include?(".xcworkspace")
19
+ end),
20
+ FastlaneCore::ConfigItem.new(key: :project,
21
+ short_option: "-p",
22
+ optional: true,
23
+ env_name: "SNAPSHOT_PROJECT",
24
+ description: "Path the project file",
25
+ verify_block: proc do |value|
26
+ v = File.expand_path(value.to_s)
27
+ raise "Project file not found at path '#{v}'".red unless File.exist?(v)
28
+ raise "Project file invalid".red unless File.directory?(v)
29
+ raise "Project file is not a project file, must end with .xcodeproj".red unless v.include?(".xcodeproj")
30
+ end),
31
+ FastlaneCore::ConfigItem.new(key: :devices,
32
+ description: "A list of devices you want to take the screenshots from",
33
+ is_string: false,
34
+ optional: true,
35
+ verify_block: proc do |value|
36
+ raise "Devices must be an array" unless value.kind_of?(Array)
37
+ available = FastlaneCore::Simulator.all
38
+ value.each do |current|
39
+ unless available.any? { |d| d.name.strip == current.strip }
40
+ raise "Device '#{current}' not in list of available simulators '#{available.join(', ')}'".red
41
+ end
42
+ end
43
+ end),
44
+ FastlaneCore::ConfigItem.new(key: :languages,
45
+ description: "A list of languages which should be used",
46
+ is_string: false,
47
+ default_value: [
48
+ 'en-US'
49
+ ]),
50
+ FastlaneCore::ConfigItem.new(key: :output_directory,
51
+ short_option: "-o",
52
+ env_name: "SNAPSHOT_OUTPUT_DIRECTORY",
53
+ description: "The directory where to store the screenshots",
54
+ default_value: output_directory),
55
+ FastlaneCore::ConfigItem.new(key: :ios_version,
56
+ description: "By default, the latest version should be used automatically. If you want to change it, do it here",
57
+ default_value: Snapshot::LatestIosVersion.version),
58
+ FastlaneCore::ConfigItem.new(key: :stop_after_first_error,
59
+ env_name: 'SNAPSHOT_BREAK_ON_FIRST_ERROR',
60
+ description: "Should snapshot stop immediately after one of the tests failed on one device?",
61
+ default_value: false,
62
+ is_string: false),
63
+ FastlaneCore::ConfigItem.new(key: :skip_open_summary,
64
+ env_name: 'SNAPSHOT_SKIP_OPEN_SUMMARY',
65
+ description: "Don't open the HTML summary after running `snapshot`",
66
+ default_value: false,
67
+ is_string: false),
68
+ FastlaneCore::ConfigItem.new(key: :clear_previous_screenshots,
69
+ env_name: 'SNAPSHOT_CLEAR_PREVIOUS_SCREENSHOTS',
70
+ description: "Enabling this option will automatically clear previously generated screenshots before running snapshot",
71
+ default_value: false,
72
+ is_string: false),
73
+
74
+ # Everything around building
75
+ FastlaneCore::ConfigItem.new(key: :buildlog_path,
76
+ short_option: "-l",
77
+ env_name: "SNAPSHOT_BUILDLOG_PATH",
78
+ description: "The directory where to store the build log",
79
+ default_value: "~/Library/Logs/snapshot"),
80
+ FastlaneCore::ConfigItem.new(key: :configuration,
81
+ short_option: "-q",
82
+ env_name: "SNAPSHOT_CONFIGURATION",
83
+ description: "The configuration to use when building the app. Defaults to 'Release'",
84
+ optional: true),
85
+ FastlaneCore::ConfigItem.new(key: :sdk,
86
+ short_option: "-k",
87
+ env_name: "SNAPSHOT_SDK",
88
+ description: "The SDK that should be used for building the application",
89
+ optional: true),
90
+ FastlaneCore::ConfigItem.new(key: :scheme,
91
+ short_option: "-s",
92
+ env_name: 'SNAPSHOT_SCHEME',
93
+ description: "The scheme you want to use, this must be the scheme for the UI Tests",
94
+ optional: true) # optional true because we offer a picker to the user
95
+ ]
96
+ end
97
+ end
98
+ end
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title><%= @title %></title>
4
+ <title>KrauseFx/snapshot</title>
5
5
  <meta charset="UTF-8">
6
6
  <style type="text/css">
7
7
  * {
@@ -4,30 +4,25 @@ require 'fastimage'
4
4
  module Snapshot
5
5
  class ReportsGenerator
6
6
  def generate
7
+ Helper.log.info "Generating HTML Report"
7
8
 
8
- config = SnapshotConfig.shared_instance
9
- screens_path = config.screenshots_path
9
+ screens_path = Snapshot.config[:output_directory]
10
10
 
11
- @title = config.html_title
12
11
  @data = {}
13
12
 
14
- Dir["#{screens_path}/*"].sort.each do |language_path|
15
- language = File.basename(language_path)
16
- Dir[File.join(language_path, '*')].sort.each do |screenshot|
13
+ Dir[File.join(screens_path, "*")].sort.each do |language_folder|
14
+ language = File.basename(language_folder)
15
+ Dir[File.join(language_folder, '*.png')].sort.each do |screenshot|
16
+ available_devices.each do |key_name, output_name|
17
+ next unless File.basename(screenshot).include?(key_name)
17
18
 
18
- ["portrait", "landscape"].each do |orientation|
19
- available_devices.each do |key_name, output_name|
20
- if File.basename(screenshot).include?key_name and File.basename(screenshot).include?orientation
21
- output_name += " (#{orientation.capitalize})"
22
- # This screenshot it from this device
23
- @data[language] ||= {}
24
- @data[language][output_name] ||= []
19
+ # This screenshot is from this device
20
+ @data[language] ||= {}
21
+ @data[language][output_name] ||= []
25
22
 
26
- resulting_path = File.join('.', language, File.basename(screenshot))
27
- @data[language][output_name] << resulting_path
28
- break # to not include iPhone 6 and 6 Plus (name is contained in the other name)
29
- end
30
- end
23
+ resulting_path = File.join('.', language, File.basename(screenshot))
24
+ @data[language][output_name] << resulting_path
25
+ break # to not include iPhone 6 and 6 Plus (name is contained in the other name)
31
26
  end
32
27
  end
33
28
  end
@@ -44,25 +39,26 @@ module Snapshot
44
39
  end
45
40
 
46
41
  private
47
- def lib_path
48
- if not Helper.is_test? and Gem::Specification::find_all_by_name('snapshot').any?
49
- return [Gem::Specification.find_by_name('snapshot').gem_dir, 'lib'].join('/')
50
- else
51
- return './lib'
52
- end
53
- end
54
42
 
55
- def available_devices
56
- # The order IS important, since those names are used to check for include?
57
- # and the iPhone 6 is inlucded in the iPhone 6 Plus
58
- {
59
- 'iPhone6Plus' => "iPhone 6 Plus",
60
- 'iPhone6' => "iPhone 6",
61
- 'iPhone5' => "iPhone 5",
62
- 'iPhone4' => "iPhone 4",
63
- 'iPad' => "iPad",
64
- 'Mac' => "Mac"
65
- }
43
+ def lib_path
44
+ if !Helper.is_test? and Gem::Specification.find_all_by_name('snapshot').any?
45
+ return [Gem::Specification.find_by_name('snapshot').gem_dir, 'lib'].join('/')
46
+ else
47
+ return './lib'
66
48
  end
49
+ end
50
+
51
+ def available_devices
52
+ # The order IS important, since those names are used to check for include?
53
+ # and the iPhone 6 is inlucded in the iPhone 6 Plus
54
+ {
55
+ 'iPhone6Plus' => "iPhone 6 Plus",
56
+ 'iPhone6' => "iPhone 6",
57
+ 'iPhone5' => "iPhone 5",
58
+ 'iPhone4' => "iPhone 4",
59
+ 'iPad' => "iPad",
60
+ 'Mac' => "Mac"
61
+ }
62
+ end
67
63
  end
68
- end
64
+ end
@@ -21,11 +21,11 @@ module Snapshot
21
21
  all_devices.split("\n").each do |line|
22
22
  parsed = line.match(/\s+([\w\s]+)\s\(([\w\-]+)\)/) || []
23
23
  next unless parsed.length == 3 # we don't care about those headers
24
- parsed, name, id = parsed.to_a
24
+ _, name, id = parsed.to_a
25
25
  puts "Removing device #{name} (#{id})"
26
26
  `xcrun simctl delete #{id}`
27
27
  end
28
-
28
+
29
29
  all_device_types = `xcrun simctl list devicetypes`.scan(/(.*)\s\((.*)\)/)
30
30
  # == Device Types ==
31
31
  # iPhone 4s (com.apple.CoreSimulator.SimDeviceType.iPhone-4s)
@@ -33,7 +33,7 @@ module Snapshot
33
33
  # iPhone 5s (com.apple.CoreSimulator.SimDeviceType.iPhone-5s)
34
34
  # iPhone 6 (com.apple.CoreSimulator.SimDeviceType.iPhone-6)
35
35
  all_device_types.each do |device_type|
36
- next if device_type.join(' ').include?"Watch" # we don't want to deal with the Watch right now
36
+ next if device_type.join(' ').include? "Watch" # we don't want to deal with the Watch right now
37
37
 
38
38
  ios_versions.each do |ios_version|
39
39
  puts "Creating #{device_type} for iOS version #{ios_version}"
@@ -42,4 +42,4 @@ module Snapshot
42
42
  end
43
43
  end
44
44
  end
45
- end
45
+ end
@@ -1,275 +1,88 @@
1
- require 'pty'
2
1
  require 'shellwords'
2
+ require 'plist'
3
3
 
4
4
  module Snapshot
5
5
  class Runner
6
- TRACE_DIR = '/tmp/snapshot_traces'
6
+ attr_accessor :number_of_retries
7
7
 
8
- def work(clean: true, build: true, take_snapshots: true)
9
- SnapshotConfig.shared_instance.js_file # to verify the file can be found earlier
8
+ def work
9
+ FastlaneCore::PrintTable.print_values(config: Snapshot.config, hide_keys: [], title: "Summary")
10
10
 
11
- Builder.new.build_app(clean: clean) if build
12
- @app_path = determine_app_path
11
+ clear_previous_screenshots if Snapshot.config[:clear_previous_screenshots]
13
12
 
14
- counter = 0
15
- errors = []
16
-
17
- if (SnapshotConfig.shared_instance.clear_previous_screenshots and take_snapshots)
18
- path_to_clear = (SnapshotConfig.shared_instance.screenshots_path + "/*-*/*.png") # languages always contain a `-`
19
- Dir[path_to_clear].each { |a| File.delete(a) } # no idea why rm_rf doesn't work
20
- end
13
+ Helper.log.info "Building and running project - this might take some time...".green
21
14
 
22
- SnapshotConfig.shared_instance.devices.each do |device|
23
- SnapshotConfig.shared_instance.languages.each do |language_item|
24
-
25
- if language_item.instance_of?String
26
- language = language_item
27
- locale = language_item
28
- else
29
- (language, locale) = language_item
30
- end
31
-
32
-
33
- prepare_simulator(device, language)
34
-
35
- reinstall_app(device, language, locale) unless ENV["SNAPSHOT_SKIP_UNINSTALL"]
36
-
15
+ self.number_of_retries = 0
16
+ errors = []
17
+ Snapshot.config[:devices].each do |device|
18
+ Snapshot.config[:languages].each do |language|
37
19
  begin
38
- errors.concat(run_tests(device, language, locale))
20
+ launch(language, device)
39
21
  rescue => ex
40
- Helper.log.error(ex)
41
- end
42
-
43
- # we also want to see the screenshots when something went wrong
44
- counter += copy_screenshots(language) if take_snapshots
45
-
46
- teardown_simulator(device, language)
47
-
48
- break if errors.any? && ENV["SNAPSHOT_BREAK_ON_FIRST_ERROR"]
49
- end
50
-
51
- break if errors.any? && ENV["SNAPSHOT_BREAK_ON_FIRST_ERROR"]
52
- end
53
-
54
- kill_simulator # close the simulator after the script is finished
55
-
56
- return unless take_snapshots
57
-
58
- ReportsGenerator.new.generate
59
-
60
- if errors.count > 0
61
- Helper.log.error "-----------------------------------------------------------"
62
- Helper.log.error errors.join(' - ').red
63
- Helper.log.error "-----------------------------------------------------------"
64
- raise "Finished generating #{counter} screenshots with #{errors.count} errors.".red
65
- else
66
- Helper.log.info "Successfully finished generating #{counter} screenshots.".green
67
- end
68
-
69
- Helper.log.info "Check it out here: #{SnapshotConfig.shared_instance.screenshots_path}".green
70
- end
71
-
72
- def clean_old_traces
73
- FileUtils.rm_rf(TRACE_DIR)
74
- FileUtils.mkdir_p(TRACE_DIR)
75
- end
76
-
77
- def prepare_simulator(device, language)
78
- SnapshotConfig.shared_instance.blocks[:setup_for_device_change].call(device, udid_for_simulator(device), language) # Callback
79
- SnapshotConfig.shared_instance.blocks[:setup_for_language_change].call(language, device) # deprecated
80
- end
81
-
82
- def teardown_simulator(device, language)
83
- SnapshotConfig.shared_instance.blocks[:teardown_language].call(language, device) # Callback
84
- SnapshotConfig.shared_instance.blocks[:teardown_device].call(device, language) # deprecated
85
- end
86
-
87
- def udid_for_simulator(name) # fetches the UDID of the simulator type
88
- all = Simulators.raw_simulators.split("\n")
89
- all.each do |current|
90
- return current.match(/\[(.*)\]/)[1] if current.include?name
91
- end
92
- raise "Could not find simulator '#{name}' to install the app on."
93
- end
94
-
95
- def reinstall_app(device, language, locale)
96
- Helper.log.info "Reinstalling app...".yellow unless $verbose
22
+ Helper.log.error ex # we should to show right here as well
23
+ errors << ex
97
24
 
98
- app_identifier = ENV["SNAPSHOT_APP_IDENTIFIER"]
99
- app_identifier ||= @app_identifier
100
-
101
- def com(cmd)
102
- puts cmd.magenta if $verbose
103
-
104
- IO.popen("#{cmd} 2>&1", err: [:child, :out]) do |io|
105
- io.each do |line|
106
- puts line if (line.to_s.length > 0 and $verbose)
25
+ raise ex if Snapshot.config[:stop_after_first_error]
107
26
  end
108
- io.close
109
27
  end
110
28
  end
111
29
 
112
- udid = udid_for_simulator(device)
113
-
114
- kill_simulator
115
-
116
- if Snapshot.min_xcode7?
117
- clean_old_traces
118
- com("xcrun instruments -w '#{udid}' -t 'Blank' -l 1 -D '#{TRACE_DIR}/trace'")
119
- else
120
- com("xcrun simctl boot '#{udid}'")
121
- end
30
+ raise errors.join('; ') if errors.count > 0
122
31
 
123
- com("xcrun simctl uninstall booted '#{app_identifier}'")
124
- com("xcrun simctl install booted #{@app_path.shellescape}")
125
- com("xcrun simctl shutdown booted") unless Snapshot.min_xcode7?
32
+ # Generate HTML report
33
+ ReportsGenerator.new.generate
126
34
  end
127
35
 
128
- def run_tests(device, language, locale)
129
- Helper.log.info "Running tests on #{device} in language #{language} (locale #{locale})".green
130
-
131
- clean_old_traces
36
+ def launch(language, device_type)
37
+ screenshots_path = TestCommandGenerator.derived_data_path
38
+ FileUtils.rm_rf(screenshots_path)
39
+ FileUtils.mkdir_p(screenshots_path)
132
40
 
133
- ENV['SNAPSHOT_LANGUAGE'] = language
134
- command = generate_test_command(device, language, locale)
135
- Helper.log.debug command.yellow
136
-
137
- retry_run = false
41
+ File.write("/tmp/language.txt", language)
138
42
 
139
- lines = []
140
- errors = []
141
- PTY.spawn(command) do |stdout, stdin, pid|
43
+ command = TestCommandGenerator.generate(device_type: device_type)
142
44
 
143
- # Waits for process so that we can see if anything has failed
144
- begin
145
- stdout.sync
45
+ Helper.log_alert("#{device_type} - #{language}")
146
46
 
147
- stdout.each do |line|
148
- lines << line
149
- begin
150
- puts line.strip if $verbose
151
- result = parse_test_line(line)
152
- case result
153
- when :retry
154
- retry_run = true
155
- when :screenshot
156
- Helper.log.info "Successfully took screenshot 📱"
157
- when :pass
158
- Helper.log.info line.strip.gsub("Pass:", "✓").green
159
- when :fail
160
- Helper.log.info line.strip.gsub("Fail:", "✗").red
161
- when :need_permission
162
- raise "Looks like you may need to grant permission for Instruments to analyze other processes.\nPlease Ctrc + C and run this command: \"#{command}\""
163
- end
164
- rescue Exception => ex
165
- Helper.log.error lines.join('')
166
- Helper.log.error ex.to_s.red
167
- errors << ex.to_s
168
- end
47
+ prefix_hash = [
48
+ {
49
+ prefix: "Running Tests: ",
50
+ block: proc do |value|
51
+ value.include?("Touching")
169
52
  end
170
-
171
- rescue Errno::EIO => e
172
- # We could maybe do something like this
173
- ensure
174
- ::Process.wait pid
175
- end
176
-
177
- end
178
-
179
- if retry_run
180
- Helper.log.error "Instruments tool failed again. Re-trying..." if $verbose
181
- sleep 2 # We need enough sleep... that's an instruments bug
182
- errors = run_tests(device, language, locale)
183
- end
184
-
185
- return errors
53
+ }
54
+ ]
55
+
56
+ FastlaneCore::CommandExecutor.execute(command: command,
57
+ print_all: true,
58
+ print_command: true,
59
+ prefix: prefix_hash,
60
+ loading: "Loading...",
61
+ error: proc do |output, return_code|
62
+ ErrorHandler.handle_test_error(output, return_code)
63
+
64
+ # no exception raised... that means we need to retry
65
+ Helper.log.info "Cought error... #{return_code}".red
66
+
67
+ self.number_of_retries += 1
68
+ if self.number_of_retries < 20
69
+ launch(language, device_type)
70
+ else
71
+ # It's important to raise an error, as we don't want to collect the screenshots
72
+ raise "Too many errors... no more retries...".red
73
+ end
74
+ end)
75
+
76
+ raw_output = File.read(TestCommandGenerator.xcodebuild_log_path)
77
+ Collector.fetch_screenshots(raw_output, language, device_type)
186
78
  end
187
79
 
188
- def determine_app_path
189
- # Determine the path to the actual app and not the WatchKit app
190
- build_dir = SnapshotConfig.shared_instance.build_dir || '/tmp/snapshot'
191
- Dir.glob("#{build_dir}/**/*.app/*.plist").each do |path|
192
- # `2>&1` to hide the error if it's not there: http://stackoverflow.com/a/4783536/445598
193
- watchkit_enabled = `/usr/libexec/PlistBuddy -c 'Print WKWatchKitApp' '#{path}' 2>&1`.strip
194
- next if watchkit_enabled == 'true' # we don't care about WatchKit Apps
195
-
196
- app_identifier = `/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' '#{path}' 2>&1`.strip
197
- if app_identifier and app_identifier.length > 0
198
- # This seems to be the valid Info.plist
199
- @app_identifier = app_identifier
200
- return File.expand_path("..", path) # the app
201
- end
80
+ def clear_previous_screenshots
81
+ Helper.log.info "Clearing previously generated screenshots".yellow
82
+ path = File.join(".", Snapshot.config[:output_directory], "*", "*.png")
83
+ Dir[path].each do |current|
84
+ File.delete(current)
202
85
  end
203
-
204
- raise "Could not find app in '#{build_dir}'. Make sure you're following the README and set the build directory to the correct path.".red
205
- end
206
-
207
- def parse_test_line(line)
208
- if line =~ /.*Target failed to run.*/
209
- return :retry
210
- elsif line.include?"segmentation fault" # a new bug introduced with Xcode 7
211
- return :retry
212
- elsif line.include?"Timed out waiting" # a new bug introduced with Xcode 7
213
- `killall "iOS Simulator"`
214
- `killall "Simulator"`
215
- return :retry
216
- elsif line.include?"Screenshot captured"
217
- return :screenshot
218
- elsif line.include? "Instruments wants permission to analyze other processes"
219
- return :need_permission
220
- elsif line.include? "Pass: "
221
- return :pass
222
- elsif line.include? "Fail: "
223
- return :fail
224
- elsif line =~ /.*Error: (.*)/
225
- raise "UIAutomation Error: #{$1}"
226
- elsif line =~ /Instruments Usage Error :(.*)/
227
- raise "Instruments Usage Error: #{$1}"
228
- elsif line.include?"__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object"
229
- raise "Looks like something is wrong with the used app. Make sure the build was successful."
230
- end
231
- end
232
-
233
- def kill_simulator
234
- `xcrun simctl shutdown booted`
235
- `killall "iOS Simulator"`
236
- `killall "Simulator"`
237
- end
238
-
239
- def copy_screenshots(language)
240
- resulting_path = File.join(SnapshotConfig.shared_instance.screenshots_path, language)
241
-
242
- FileUtils.mkdir_p resulting_path
243
-
244
- unless SnapshotConfig.shared_instance.skip_alpha_removal
245
- ScreenshotFlatten.new.run(TRACE_DIR)
246
- end
247
-
248
- ScreenshotRotate.new.run(TRACE_DIR)
249
-
250
- Dir.glob("#{TRACE_DIR}/**/*.png") do |file|
251
- FileUtils.cp_r(file, resulting_path + '/')
252
- end
253
- return Dir.glob("#{TRACE_DIR}/**/*.png").count
254
- end
255
-
256
- def generate_test_command(device, language, locale)
257
- is_ipad = (device.downcase.include?'ipad')
258
- script_path = SnapshotConfig.shared_instance.js_file(is_ipad)
259
- custom_run_args = SnapshotConfig.shared_instance.custom_run_args || ENV["SNAPSHOT_CUSTOM_RUN_ARGS"] || ''
260
-
261
- [
262
- "instruments",
263
- "-w '#{device}'",
264
- "-D '#{TRACE_DIR}/trace'",
265
- "-t 'Automation'",
266
- "#{@app_path.shellescape}",
267
- "-e UIARESULTSPATH '#{TRACE_DIR}'",
268
- "-e UIASCRIPT '#{script_path}'",
269
- "-AppleLanguages '(#{language})'",
270
- "-AppleLocale '#{locale}'",
271
- custom_run_args,
272
- ].join(' ')
273
86
  end
274
87
  end
275
88
  end