Class: Snapshot::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/snapshot/runner.rb

Constant Summary collapse

TRACE_DIR =
'/tmp/snapshot_traces'

Instance Method Summary collapse

Instance Method Details

#clean_old_tracesObject



72
73
74
75
# File 'lib/snapshot/runner.rb', line 72

def clean_old_traces
  FileUtils.rm_rf(TRACE_DIR)
  FileUtils.mkdir_p(TRACE_DIR)
end

#com(cmd) ⇒ Object



101
102
103
104
105
106
107
# File 'lib/snapshot/runner.rb', line 101

def com(cmd)
  puts cmd.magenta if $verbose
  Open3.popen3("#{cmd} 2>&1") do |stdin, stdout, stderr, wait_thr|
    result = stdout.read
    puts result if (result.to_s.length > 0 and $verbose)
  end
end

#copy_screenshots(language) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/snapshot/runner.rb', line 224

def copy_screenshots(language)
  resulting_path = File.join(SnapshotConfig.shared_instance.screenshots_path, language)

  FileUtils.mkdir_p resulting_path

  unless SnapshotConfig.shared_instance.skip_alpha_removal
    ScreenshotFlatten.new.run(TRACE_DIR)
  end

  ScreenshotRotate.new.run(TRACE_DIR)

  Dir.glob("#{TRACE_DIR}/**/*.png") do |file|
    FileUtils.cp_r(file, resulting_path + '/')
  end
  return Dir.glob("#{TRACE_DIR}/**/*.png").count
end

#determine_app_pathObject



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/snapshot/runner.rb', line 180

def determine_app_path
  # Determine the path to the actual app and not the WatchKit app
  build_dir = SnapshotConfig.shared_instance.build_dir || '/tmp/snapshot'
  Dir.glob("#{build_dir}/**/*.app/*.plist").each do |path|
    # `2>&1` to hide the error if it's not there: http://stackoverflow.com/a/4783536/445598
    watchkit_enabled = `/usr/libexec/PlistBuddy -c 'Print WKWatchKitApp' '#{path}' 2>&1`.strip
    next if watchkit_enabled == 'true' # we don't care about WatchKit Apps

    app_identifier = `/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' '#{path}' 2>&1`.strip
    if app_identifier and app_identifier.length > 0
      # This seems to be the valid Info.plist
      @app_identifier = app_identifier
      return File.expand_path("..", path) # the app
    end
  end

  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
end

#generate_test_command(device, language, locale) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/snapshot/runner.rb', line 241

def generate_test_command(device, language, locale)
  is_ipad = (device.downcase.include?'ipad')
  script_path = SnapshotConfig.shared_instance.js_file(is_ipad)
  custom_run_args = SnapshotConfig.shared_instance.custom_run_args || ENV["SNAPSHOT_CUSTOM_RUN_ARGS"] || ''

  [
    "instruments",
    "-w '#{device}'",
    "-D '#{TRACE_DIR}/trace'",
    "-t 'Automation'",
    "#{@app_path.shellescape}",
    "-e UIARESULTSPATH '#{TRACE_DIR}'",
    "-e UIASCRIPT '#{script_path}'",
    "-AppleLanguages '(#{language})'",
    "-AppleLocale '#{locale}'",
    custom_run_args,
  ].join(' ')
end

#parse_test_line(line) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/snapshot/runner.rb', line 199

def parse_test_line(line)
  if line =~ /.*Target failed to run.*/
    return :retry
  elsif line.include?"segmentation fault" # a new bug introduced with Xcode 7
    return :retry
  elsif line.include?"Timed out waiting" # a new bug introduced with Xcode 7
    `killall "iOS Simulator"`
    return :retry
  elsif line.include?"Screenshot captured"
    return :screenshot
  elsif line.include? "Instruments wants permission to analyze other processes"
    return :need_permission
  elsif line.include? "Pass: "
    return :pass
  elsif line.include? "Fail: "
    return :fail
  elsif line =~ /.*Error: (.*)/
    raise "UIAutomation Error: #{$1}"
  elsif line =~ /Instruments Usage Error :(.*)/
    raise "Instruments Usage Error: #{$1}"
  elsif line.include?"__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object"
    raise "Looks like something is wrong with the used app. Make sure the build was successful."
  end
end

#prepare_simulator(device, language) ⇒ Object



77
78
79
80
# File 'lib/snapshot/runner.rb', line 77

def prepare_simulator(device, language)
  SnapshotConfig.shared_instance.blocks[:setup_for_device_change].call(device, udid_for_simulator(device), language)  # Callback
  SnapshotConfig.shared_instance.blocks[:setup_for_language_change].call(language, device) # deprecated
end

#reinstall_app(device, language, locale) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/snapshot/runner.rb', line 95

def reinstall_app(device, language, locale)
  Helper.log.info "Reinstalling app...".yellow unless $verbose

  app_identifier = ENV["SNAPSHOT_APP_IDENTIFIER"]
  app_identifier ||= @app_identifier

  def com(cmd)
    puts cmd.magenta if $verbose
    Open3.popen3("#{cmd} 2>&1") do |stdin, stdout, stderr, wait_thr|
      result = stdout.read
      puts result if (result.to_s.length > 0 and $verbose)
    end
  end

  udid = udid_for_simulator(device)

  com("killall 'iOS Simulator'")
  sleep 3
  com("xcrun simctl boot '#{udid}'")
  com("xcrun simctl uninstall booted '#{app_identifier}'")
  sleep 3
  com("xcrun simctl install booted '#{@app_path.shellescape}'")
  com("xcrun simctl shutdown booted")
end

#run_tests(device, language, locale) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/snapshot/runner.rb', line 120

def run_tests(device, language, locale)
  Helper.log.info "Running tests on #{device} in language #{language} (locale #{locale})".green
  
  clean_old_traces

  ENV['SNAPSHOT_LANGUAGE'] = language
  command = generate_test_command(device, language, locale)
  Helper.log.debug command.yellow
  
  retry_run = false

  lines = []
  errors = []
  PTY.spawn(command) do |stdout, stdin, pid|

    # Waits for process so that we can see if anything has failed
    begin
      stdout.sync

      stdout.each do |line|
        lines << line
        begin
          puts line.strip if $verbose
          result = parse_test_line(line)
          case result
            when :retry
              retry_run = true
            when :screenshot
              Helper.log.info "Successfully took screenshot 📱"
            when :pass
              Helper.log.info line.strip.gsub("Pass:", "✓").green
            when :fail
              Helper.log.info line.strip.gsub("Fail:", "✗").red
            when :need_permission
              raise "Looks like you may need to grant permission for Instruments to analyze other processes.\nPlease Ctrc + C and run this command: \"#{command}\""
            end
        rescue Exception => ex
          Helper.log.error lines.join('')
          Helper.log.error ex.to_s.red
          errors << ex.to_s
        end
      end

    rescue Errno::EIO => e
      # We could maybe do something like this
    ensure
      ::Process.wait pid
    end

  end

  if retry_run
    Helper.log.error "Instruments tool failed again. Re-trying..." if $verbose
    sleep 2 # We need enough sleep... that's an instruments bug
    errors = run_tests(device, language, locale)
  end

  return errors
end

#teardown_simulator(device, language) ⇒ Object



82
83
84
85
# File 'lib/snapshot/runner.rb', line 82

def teardown_simulator(device, language)
  SnapshotConfig.shared_instance.blocks[:teardown_language].call(language, device) # Callback
  SnapshotConfig.shared_instance.blocks[:teardown_device].call(device, language) # deprecated
end

#udid_for_simulator(name) ⇒ Object

fetches the UDID of the simulator type



87
88
89
90
91
92
93
# File 'lib/snapshot/runner.rb', line 87

def udid_for_simulator(name) # fetches the UDID of the simulator type
  all = Simulators.raw_simulators.split("\n")
  all.each do |current|
    return current.match(/\[(.*)\]/)[1] if current.include?name
  end
  raise "Could not find simulator '#{name}' to install the app on."
end

#work(clean: true, build: true, take_snapshots: true) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/snapshot/runner.rb', line 8

def work(clean: true, build: true, take_snapshots: true)
  SnapshotConfig.shared_instance.js_file # to verify the file can be found earlier

  Builder.new.build_app(clean: clean) if build
  @app_path = determine_app_path

  counter = 0
  errors = []

  if (SnapshotConfig.shared_instance.clear_previous_screenshots and take_snapshots)
    path_to_clear = (SnapshotConfig.shared_instance.screenshots_path + "/*-*/*.png") # languages always contain a `-`
    Dir[path_to_clear].each { |a| File.delete(a) } # no idea why rm_rf doesn't work
  end

  SnapshotConfig.shared_instance.devices.each do |device|
    SnapshotConfig.shared_instance.languages.each do |language_item|

      if language_item.instance_of?String
        language = language_item
        locale = language_item
      else
        (language, locale) = language_item            
      end


      prepare_simulator(device, language)

      reinstall_app(device, language, locale) unless ENV["SNAPSHOT_SKIP_UNINSTALL"]
      
      begin
        errors.concat(run_tests(device, language, locale))
      rescue => ex
        Helper.log.error(ex)
      end

      # we also want to see the screenshots when something went wrong
      counter += copy_screenshots(language) if take_snapshots 

      teardown_simulator(device, language)

      break if errors.any? && ENV["SNAPSHOT_BREAK_ON_FIRST_ERROR"]
    end

    break if errors.any? && ENV["SNAPSHOT_BREAK_ON_FIRST_ERROR"]
  end

  `killall "iOS Simulator"` # close the simulator after the script is finished

  return unless take_snapshots

  ReportsGenerator.new.generate

  if errors.count > 0
    Helper.log.error "-----------------------------------------------------------"
    Helper.log.error errors.join(' - ').red
    Helper.log.error "-----------------------------------------------------------"
    raise "Finished generating #{counter} screenshots with #{errors.count} errors.".red
  else
    Helper.log.info "Successfully finished generating #{counter} screenshots.".green
  end
  
  Helper.log.info "Check it out here: #{SnapshotConfig.shared_instance.screenshots_path}".green
end