illuminator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/gem/README.md +37 -0
  3. data/gem/bin/illuminatorTestRunner.rb +22 -0
  4. data/gem/lib/illuminator.rb +171 -0
  5. data/gem/lib/illuminator/argument-parsing.rb +299 -0
  6. data/gem/lib/illuminator/automation-builder.rb +39 -0
  7. data/gem/lib/illuminator/automation-runner.rb +589 -0
  8. data/gem/lib/illuminator/build-artifacts.rb +118 -0
  9. data/gem/lib/illuminator/device-installer.rb +45 -0
  10. data/gem/lib/illuminator/host-utils.rb +42 -0
  11. data/gem/lib/illuminator/instruments-runner.rb +301 -0
  12. data/gem/lib/illuminator/javascript-runner.rb +98 -0
  13. data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
  14. data/gem/lib/illuminator/listeners/full-output.rb +13 -0
  15. data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
  16. data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
  17. data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
  18. data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
  19. data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
  20. data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
  21. data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
  22. data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
  23. data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
  24. data/gem/lib/illuminator/options.rb +96 -0
  25. data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
  26. data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
  27. data/gem/lib/illuminator/test-definitions.rb +23 -0
  28. data/gem/lib/illuminator/test-suite.rb +155 -0
  29. data/gem/lib/illuminator/version.rb +3 -0
  30. data/gem/lib/illuminator/xcode-builder.rb +144 -0
  31. data/gem/lib/illuminator/xcode-utils.rb +219 -0
  32. data/gem/resources/BuildConfiguration.xcconfig +10 -0
  33. data/gem/resources/js/AppMap.js +767 -0
  34. data/gem/resources/js/Automator.js +1132 -0
  35. data/gem/resources/js/Base64.js +142 -0
  36. data/gem/resources/js/Bridge.js +102 -0
  37. data/gem/resources/js/Config.js +92 -0
  38. data/gem/resources/js/Extensions.js +2025 -0
  39. data/gem/resources/js/Illuminator.js +228 -0
  40. data/gem/resources/js/Preferences.js +24 -0
  41. data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
  42. data/gem/resources/scripts/common.applescript +25 -0
  43. data/gem/resources/scripts/diff_png.sh +61 -0
  44. data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
  45. data/gem/resources/scripts/plist_to_json.sh +40 -0
  46. data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
  47. metadata +225 -0
@@ -0,0 +1,39 @@
1
+ require 'fileutils'
2
+
3
+ require_relative './xcode-builder'
4
+ require_relative './build-artifacts'
5
+
6
+ ####################################################################################################
7
+ # Builder
8
+ ####################################################################################################
9
+
10
+ module Illuminator
11
+ class AutomationBuilder < XcodeBuilder
12
+
13
+ def initialize
14
+ super
15
+ @configuration = 'Debug'
16
+
17
+ add_environment_variable('UIAUTOMATION_BUILD', true)
18
+ end
19
+
20
+
21
+ def build_for_automation sdk, hardware_id
22
+ @xcconfig = "#{File.dirname(__FILE__)}/../../resources/BuildConfiguration.xcconfig"
23
+
24
+ preprocessor_definitions = '$(value) UIAUTOMATION_BUILD=1'
25
+ if hardware_id.nil?
26
+ @sdk = sdk || 'iphonesimulator'
27
+ @arch = @arch || 'i386'
28
+ else
29
+ @sdk = sdk || 'iphoneos'
30
+ @destination = "id=#{hardware_id}"
31
+ preprocessor_definitions += " AUTOMATION_UDID=#{hardware_id}"
32
+ end
33
+ add_environment_variable('GCC_PREPROCESSOR_DEFINITIONS', "'#{preprocessor_definitions}'")
34
+
35
+ build
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,589 @@
1
+ require 'fileutils'
2
+ require 'find'
3
+ require 'pathname'
4
+ require 'json'
5
+
6
+ require_relative './instruments-runner'
7
+ require_relative './javascript-runner'
8
+ require_relative './host-utils'
9
+ require_relative './xcode-utils'
10
+ require_relative './build-artifacts'
11
+ require_relative './test-suite'
12
+ require_relative './test-definitions'
13
+
14
+ require_relative 'listeners/pretty-output'
15
+ require_relative 'listeners/full-output'
16
+ require_relative 'listeners/console-logger'
17
+ require_relative 'listeners/test-listener'
18
+ require_relative 'listeners/saltinel-agent'
19
+
20
+ ####################################################################################################
21
+ # runner
22
+ ####################################################################################################
23
+
24
+
25
+ # responsibilities:
26
+ # - apply options to relevant components
27
+ # - prepare javascript config, and start instruments
28
+ # - process any crashes
29
+ # - run coverage
30
+ class AutomationRunner
31
+ include SaltinelAgentEventSink
32
+ include TestListenerEventSink
33
+
34
+ attr_accessor :app_name
35
+ attr_accessor :app_location
36
+
37
+ attr_reader :instruments_runner
38
+ attr_reader :javascript_runner
39
+
40
+ def initialize
41
+ @test_defs = nil
42
+ @test_suite = nil
43
+ @current_test = nil
44
+ @restarted_tests = nil
45
+ @stack_trace_lines = nil
46
+ @stack_trace_record = false
47
+ @app_crashed = false
48
+ @instruments_stopped = false
49
+ @javascript_runner = JavascriptRunner.new
50
+ @instruments_runner = InstrumentsRunner.new
51
+
52
+ @instruments_runner.add_listener("consolelogger", ConsoleLogger.new)
53
+ end
54
+
55
+
56
+ def cleanup
57
+ # start a list of what to remove
58
+ dirs_to_remove = []
59
+
60
+ # FIXME: this should probably get moved to instrument runner
61
+ # keys to the methods of the BuildArtifacts singleton that we want to remove
62
+ build_artifact_keys = [:crash_reports, :instruments, :object_files, :coverage_report_file,
63
+ :junit_report_file, :illuminator_js_runner, :illuminator_js_environment, :illuminator_config_file]
64
+ # get the directories without creating them (the 'true' arg), add them to our list
65
+ build_artifact_keys.each do |key|
66
+ dir = Illuminator::BuildArtifacts.instance.method(key).call(true)
67
+ dirs_to_remove << dir
68
+ end
69
+
70
+ # remove directories in the list
71
+ dirs_to_remove.each do |d|
72
+ dir = Illuminator::HostUtils.realpath d
73
+ puts "AutomationRunner cleanup: removing #{dir}"
74
+ FileUtils.rmtree dir
75
+ end
76
+
77
+ # run cleanups for variables we own
78
+ @instruments_runner.cleanup
79
+ # TODO: @javascript_runner cleanup?
80
+
81
+ end
82
+
83
+
84
+ def saltinel_agent_got_scenario_definitions jsonPath
85
+ return unless @test_defs.nil?
86
+ @test_defs = TestDefinitions.new jsonPath
87
+ end
88
+
89
+
90
+ def saltinel_agent_got_scenario_list jsonPath
91
+ return unless @test_suite.nil?
92
+ @restarted_tests = {}
93
+ raw_list = JSON.parse( IO.read(jsonPath) )
94
+
95
+ # create a test suite, and add test cases to it. look up class names from test defs
96
+ @test_suite = TestSuite.new(@implementation)
97
+ raw_list["scenarioNames"].each do |n|
98
+ test = @test_defs.by_name(n)
99
+ test_file_name = test["inFile"]
100
+ test_fn_name = test["definedBy"]
101
+ class_name = test_file_name.sub(".", "_") + "." + test_fn_name
102
+ @test_suite.add_test_case(class_name, n)
103
+ end
104
+ save_junit_test_report
105
+ end
106
+
107
+ def saltinel_agent_got_restart_request
108
+ puts "ILLUMINATOR FAILURE TO ORGANIZE".red if @test_suite.nil?
109
+ puts "ILLUMINATOR FAILURE TO ORGANIZE 2".red if @current_test.nil?
110
+ if @restarted_tests[@current_test]
111
+ puts "Denying restart request for previously-restarted scenario '#{@current_test}'".yellow
112
+ else
113
+ @test_suite[@current_test].reset!
114
+ @restarted_tests[@current_test] = true
115
+ @current_test = nil
116
+ @instruments_runner.force_stop "Got restart request"
117
+ end
118
+ end
119
+
120
+ def saltinel_agent_got_stacktrace_hint
121
+ @stack_trace_record = true
122
+ end
123
+
124
+ def test_listener_got_test_start name
125
+ @test_suite[@current_test].error "ILLUMINATOR FAILURE TO LISTEN" unless @current_test.nil?
126
+ @test_suite[name].reset!
127
+ @test_suite[name].start!
128
+ @current_test = name
129
+ @stack_trace_record = false
130
+ @stack_trace_lines = Array.new
131
+ end
132
+
133
+ def test_listener_got_test_pass name
134
+ puts "ILLUMINATOR FAILURE TO SORT TESTS".red unless name == @current_test
135
+ @test_suite[name].pass!
136
+ @current_test = nil
137
+ save_junit_test_report
138
+ end
139
+
140
+ def test_listener_got_test_fail message
141
+ if @test_suite.nil?
142
+ puts "Failure before test suite was received: #{message}".red
143
+ return
144
+ elsif @current_test.nil?
145
+ puts "Failure outside of a test: #{message}".red
146
+ elsif message == "The target application appears to have died"
147
+ @test_suite[@current_test].error message
148
+ @app_crashed = true
149
+ # The test runner loop will take it from here
150
+ else
151
+ @test_suite[@current_test].fail message
152
+ @test_suite[@current_test].stacktrace = @stack_trace_lines.join("\n")
153
+ @current_test = nil
154
+ save_junit_test_report
155
+ end
156
+ end
157
+
158
+ def test_listener_got_test_error message
159
+ return if @test_suite.nil?
160
+ return if @current_test.nil?
161
+ @test_suite[@current_test].fail message
162
+ @test_suite[@current_test].stacktrace = @stack_trace_lines.join("\n")
163
+ @current_test = nil
164
+ save_junit_test_report
165
+ end
166
+
167
+ def test_listener_got_line(status, message)
168
+ return if @test_suite.nil? or @current_test.nil?
169
+ line = message
170
+ line = "#{status}: #{line}" unless status.nil?
171
+ @test_suite[@current_test] << line
172
+ @stack_trace_lines << line if @stack_trace_record
173
+ end
174
+
175
+ def save_junit_test_report
176
+ f = File.open(Illuminator::BuildArtifacts.instance.junit_report_file, 'w')
177
+ f.write(@test_suite.to_xml)
178
+ f.close
179
+ end
180
+
181
+ def run_annotated_command(command)
182
+ puts "\n"
183
+ puts command.green
184
+ IO.popen command do |io|
185
+ io.each {||}
186
+ end
187
+ end
188
+
189
+ # translate input options into javascript config
190
+ def configure_javascript_runner(options, target_device_id)
191
+ js_config = @javascript_runner
192
+
193
+ js_config.target_device_id = target_device_id
194
+ js_config.is_hardware = !(options.illuminator.hardware_id.nil?)
195
+ js_config.implementation = options.javascript.implementation
196
+ js_config.test_path = options.javascript.test_path
197
+
198
+ js_config.entry_point = options.illuminator.entry_point
199
+ js_config.scenario_list = options.illuminator.test.names
200
+ js_config.tags_any = options.illuminator.test.tags.any
201
+ js_config.tags_all = options.illuminator.test.tags.all
202
+ js_config.tags_none = options.illuminator.test.tags.none
203
+ js_config.random_seed = options.illuminator.test.random_seed
204
+
205
+ js_config.sim_device = options.simulator.device
206
+ js_config.sim_version = options.simulator.version
207
+
208
+ js_config.app_specific_config = options.javascript.app_specific_config
209
+
210
+ # don't offset the numbers this time
211
+ js_config.scenario_number_offset = 0
212
+
213
+ # write main config
214
+ js_config.write_configuration()
215
+ end
216
+
217
+
218
+ def configure_javascript_rerunner(scenarios_to_run, number_offset)
219
+ js_config = @javascript_runner
220
+ js_config.random_seed = nil
221
+ js_config.entry_point = "runTestsByName"
222
+ js_config.scenario_list = scenarios_to_run
223
+ js_config.scenario_number_offset = number_offset
224
+
225
+ js_config.write_configuration()
226
+ end
227
+
228
+
229
+ def configure_instruments_runner_listeners(options)
230
+ test_listener = TestListener.new
231
+ test_listener.event_sink = self
232
+ @instruments_runner.add_listener("test_listener", test_listener)
233
+
234
+ # listener to provide screen output
235
+ if options.instruments.do_verbose
236
+ @instruments_runner.add_listener("consoleoutput", FullOutput.new)
237
+ else
238
+ @instruments_runner.add_listener("consoleoutput", PrettyOutput.new)
239
+ end
240
+ end
241
+
242
+ def configure_instruments_runner(options)
243
+ # set up instruments and get target device ID
244
+ @instruments_runner.startup_timeout = options.instruments.timeout
245
+ @instruments_runner.hardware_id = options.illuminator.hardware_id
246
+ @instruments_runner.app_location = @app_location
247
+
248
+ if options.illuminator.hardware_id.nil?
249
+ @instruments_runner.sim_language = options.simulator.language
250
+ @instruments_runner.sim_device = Illuminator::XcodeUtils.instance.get_simulator_id(options.simulator.device,
251
+ options.simulator.version)
252
+ end
253
+
254
+ # max silence is the timeout times 5 unless otherwise specified
255
+ @instruments_runner.max_silence = options.instruments.max_silence
256
+ @instruments_runner.max_silence ||= options.instruments.timeout * 5
257
+
258
+ # setup listeners on instruments
259
+ configure_instruments_runner_listeners(options)
260
+ end
261
+
262
+ def configure_target_device(options)
263
+ unless options.illuminator.hardware_id.nil?
264
+ puts "Using hardware_id = '#{options.illuminator.hardware_id}' instead of simulator".green
265
+ target_device_id = options.illuminator.hardware_id
266
+ else
267
+ if @instruments_runner.sim_device.nil?
268
+ msg = "Could not find a simulator for device='#{options.simulator.device}', version='#{options.simulator.version}'"
269
+ puts msg.red
270
+ puts Illuminator::XcodeUtils.instance.get_simulator_devices.yellow
271
+ raise ArgumentError, msg
272
+ end
273
+ target_device_id = @instruments_runner.sim_device
274
+ end
275
+
276
+ # reset the simulator if desired
277
+ Illuminator::XcodeUtils.kill_all_simulator_processes
278
+ if options.illuminator.hardware_id.nil? and options.illuminator.task.set_sim
279
+ Illuminator::XcodeUtils.instance.reset_simulator target_device_id
280
+ end
281
+
282
+ target_device_id
283
+ end
284
+
285
+ def run_with_options(options)
286
+
287
+ # Sanity checks
288
+ raise ArgumentError, 'Entry point was not supplied' if options.illuminator.entry_point.nil?
289
+ raise ArgumentError, 'Path to all tests was not supplied' if options.javascript.test_path.nil?
290
+ raise ArgumentError, 'Implementation was not supplied' if options.javascript.implementation.nil?
291
+
292
+ @app_name = options.xcode.app_name
293
+ @app_location = options.instruments.app_location
294
+ @implementation = options.javascript.implementation
295
+
296
+ # set up instruments and get target device ID
297
+ configure_instruments_runner(options)
298
+ target_device_id = configure_target_device(options)
299
+
300
+ start_time = Time.now
301
+ @test_suite = nil
302
+
303
+ # run the first time
304
+ instruments_exit_status = execute_entire_test_suite(options, target_device_id, nil)
305
+
306
+ # rerun if specified. do not rerun if @testsuite wasn't received (indicating setup problems)
307
+ if instruments_exit_status.fatal_error
308
+ puts "Fatal error: #{instruments_exit_status.fatal_reason}".red
309
+ elsif options.illuminator.test.retest.attempts.nil? or @test_suite.nil?
310
+ # nothing to do
311
+ else
312
+ execute_test_suite_reruns(options, target_device_id)
313
+ end
314
+
315
+ # annotate the run
316
+ total_time = Time.at(Time.now - start_time).gmtime.strftime("%H:%M:%S")
317
+ puts "Automation completed in #{total_time}".green
318
+
319
+ perform_coverage(options)
320
+
321
+ Illuminator::XcodeUtils.kill_all_simulator_processes if options.simulator.kill_after
322
+
323
+ # summarize test results to the console if desired
324
+ if "describe" == options.illuminator.entry_point
325
+ return true # no tests needed to run
326
+ else
327
+ summarize_test_results @test_suite
328
+ end
329
+
330
+ save_failed_tests_config(options, @test_suite.unpassed_tests) unless @test_suite.nil?
331
+
332
+ # TODO: exit code should be an integer, and each of these should be cases
333
+ return false if @test_suite.nil? # no tests were received
334
+ return false if 0 == @test_suite.passed_tests.length # no tests passed, or none ran
335
+ return false if 0 < @test_suite.unpassed_tests.length # 1 or more tests failed
336
+ return true
337
+ end
338
+
339
+ # perform coverage if desired and possible
340
+ def perform_coverage(options)
341
+ unless @test_suite.nil?
342
+ if options.illuminator.task.coverage #TODO: only if there are no crashes?
343
+ if Illuminator::HostUtils.which("gcovr").nil?
344
+ puts "Skipping requested coverage generation because gcovr does not appear to be in the PATH".yellow
345
+ else
346
+ generate_coverage Dir.pwd
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ # This function is for preventing infinite loops in the test run that could be caused by (e.g.)
353
+ # - an instruments max_silence error that happens ever time.
354
+ def handle_unsuccessful_instruments_run
355
+ return if @test_suite.nil?
356
+ return if @current_test.nil?
357
+
358
+ if @restarted_tests[@current_test]
359
+ @test_suite[@current_test].error("Illuminator could not get this test to complete")
360
+ save_junit_test_report
361
+ @current_test = nil
362
+ else
363
+ @restarted_tests[@current_test] = true
364
+ end
365
+ end
366
+
367
+
368
+ # run a test suite, restarting if necessary
369
+ def execute_entire_test_suite(options, target_device_id, specific_tests)
370
+
371
+ # loop until all test cases are covered.
372
+ # we won't get the actual test list until partway through -- from a listener callback
373
+ exit_status = nil
374
+ begin
375
+ remove_any_app_crashes
376
+ @app_crashed = false
377
+ @instruments_stopped = false
378
+
379
+ # Setup javascript to run the appropriate list of tests (initial or leftover)
380
+ if @test_suite.nil?
381
+ # very first attempt
382
+ configure_javascript_runner(options, target_device_id)
383
+ elsif specific_tests.nil?
384
+ # not first attempt, but we haven't made it all the way through yet
385
+ configure_javascript_rerunner(@test_suite.unstarted_tests, @test_suite.finished_tests.length)
386
+ else
387
+ # we assume that we've already gone through and have been given specific tests to check out
388
+ configure_javascript_rerunner(specific_tests, 0)
389
+ end
390
+
391
+ # Setup new saltinel listener (will overwrite the old one if it exists)
392
+ agent_listener = SaltinelAgent.new(@javascript_runner.saltinel)
393
+ agent_listener.event_sink = self
394
+ @instruments_runner.add_listener("saltinelAgent", agent_listener)
395
+
396
+ exit_status = @instruments_runner.run_once(@javascript_runner.saltinel)
397
+
398
+ if @app_crashed
399
+ handle_app_crash
400
+ elsif !exit_status.normal
401
+ handle_unsuccessful_instruments_run
402
+ end
403
+
404
+ end while not (@test_suite.nil? or @test_suite.unstarted_tests.empty? or exit_status.fatal_error)
405
+ # as long as we have a test suite with unfinished tests, and no fatal errors, keep going
406
+
407
+ exit_status
408
+ end
409
+
410
+
411
+ def execute_test_suite_reruns(options, target_device_id)
412
+ # retry any failed tests
413
+ for att in 1..options.illuminator.test.retest.attempts
414
+ unpassed_tests = @test_suite.unpassed_tests.map { |t| t.name }
415
+
416
+ # run them in batch mode if desired
417
+ unless options.illuminator.test.retest.solo
418
+ puts "Retrying failed tests in batch, attempt #{att} of #{options.illuminator.test.retest.attempts}"
419
+ execute_entire_test_suite(options, target_device_id, unpassed_tests)
420
+ else
421
+ puts "Retrying failed tests individually, attempt #{att} of #{options.illuminator.test.retest.attempts}"
422
+
423
+ unpassed_tests.each_with_index do |t, index|
424
+ test_num = index + 1
425
+ puts "Solo attempt for test #{test_num} of #{unpassed_tests.length}"
426
+ execute_entire_test_suite(options, target_device_id, [t])
427
+ end
428
+ end
429
+ end
430
+ end
431
+
432
+
433
+ # print a summary of the tests that ran, in the form ..........!.!!.!...!..@...@.!
434
+ # where periods are passing tests, exclamations are fails, and '@' symbols are crashes
435
+ def summarize_test_results test_suite
436
+ if test_suite.nil?
437
+ puts "No test cases were received from the Javascript environment; check logs for possible setup problems.".red
438
+ return
439
+ end
440
+
441
+ all_tests = test_suite.all_tests
442
+ unpassed_tests = test_suite.unpassed_tests
443
+
444
+ if 0 == all_tests.length
445
+ puts "No tests ran".yellow
446
+ elsif 0 < unpassed_tests.length
447
+ result = "Result: "
448
+ all_tests.each do |t|
449
+ if not t.ran?
450
+ result << "-"
451
+ elsif t.failed?
452
+ result << "!"
453
+ elsif t.errored?
454
+ result << "@"
455
+ else
456
+ result << "."
457
+ end
458
+ end
459
+ puts result.red
460
+ puts "#{unpassed_tests.length} of #{all_tests.length} tests FAILED".red # failed in the test suite sense
461
+ else
462
+ puts "All #{all_tests.length} tests PASSED".green
463
+ end
464
+
465
+ end
466
+
467
+ def save_failed_tests_config(options, failed_tests)
468
+ return unless 0 < failed_tests.length
469
+
470
+ # save options to re-run failed tests
471
+ new_options = options.dup
472
+ new_options.illuminator.test.random_seed = nil
473
+ new_options.illuminator.entry_point = "runTestsByName"
474
+ new_options.illuminator.test.names = failed_tests.map { |t| t.name }
475
+
476
+ Illuminator::HostUtils.save_json(new_options.to_h,
477
+ Illuminator::BuildArtifacts.instance.illuminator_rerun_failed_tests_settings)
478
+ end
479
+
480
+ def remove_any_app_crashes()
481
+ Dir.glob("#{Illuminator::XcodeUtils.instance.get_crash_directory}/#{@app_name}*.crash").each do |crash_path|
482
+ FileUtils.rmtree crash_path
483
+ end
484
+ end
485
+
486
+
487
+ def handle_app_crash
488
+ # tell the current test suite about any failures
489
+ if @current_test.nil?
490
+ puts "ILLUMINATOR FAILURE TO HANDLE APP CRASH"
491
+ return
492
+ end
493
+
494
+ # assume a crash report exists, and look for it
495
+ crashes = report_any_app_crashes
496
+
497
+ # write something useful depending on what crash reports are found
498
+ case crashes.keys.length
499
+ when 0
500
+ d = Illuminator::XcodeUtils.instance.get_crash_directory
501
+ stacktrace_text = "No crash reports found in #{d}, perhaps the app exited cleanly instead"
502
+ when 1
503
+ stacktrace_text = crashes[crashes.keys[0]]
504
+ else
505
+ stacktrace_body = crashes[crashes.keys[0]]
506
+ stacktrace_text = "Found multiple crashes: #{crashes.keys} Here is the first one:\n\n #{stacktrace_body}"
507
+ end
508
+
509
+ @test_suite[@current_test].stacktrace = stacktrace_text
510
+ @current_test = nil
511
+ save_junit_test_report
512
+ end
513
+
514
+
515
+
516
+ def report_any_app_crashes()
517
+ crash_reports_path = Illuminator::BuildArtifacts.instance.crash_reports
518
+ FileUtils.mkdir_p crash_reports_path unless File.directory?(crash_reports_path)
519
+
520
+ crashes = Hash.new
521
+ # TODO: glob if @app_name is nil
522
+ Dir.glob("#{Illuminator::XcodeUtils.instance.get_crash_directory}/#{@app_name}*.crash").each do |crash_path|
523
+ # TODO: extract process name and ignore ["launchd_sim", ...]
524
+
525
+ puts "Found a crash report from this test run at #{crash_path}"
526
+ crash_name = File.basename(crash_path, ".crash")
527
+ crash_report_path = "#{crash_reports_path}/#{crash_name}.crash"
528
+ crash_text = []
529
+ if Illuminator::XcodeUtils.instance.create_symbolicated_crash_report(@app_location, crash_path, crash_report_path)
530
+ puts "Created a symbolicated version of the crash report at #{crash_report_path}".red
531
+ else
532
+ FileUtils.cp(crash_path, crash_report_path)
533
+ puts "Copied the crash report (assumed already symbolicated) to #{crash_report_path}".red
534
+ end
535
+
536
+ # get the first few lines for the log
537
+ # TODO: possibly do error handling here just in case the file doesn't exist
538
+ file = File.open(crash_report_path, 'rb')
539
+ file.each do |line|
540
+ break if line.match(/^Binary Images/)
541
+ crash_text << line
542
+ end
543
+ file.close
544
+
545
+ crash_text << "\n"
546
+ crash_text << "Full crash report saved at #{crash_report_path}"
547
+
548
+ crashes[crash_name] = crash_text.join("")
549
+ end
550
+ crashes
551
+ end
552
+
553
+
554
+ def generate_coverage(gcWorkspace)
555
+ destination_file = Illuminator::BuildArtifacts.instance.coverage_report_file
556
+ xcode_artifacts_folder = Illuminator::BuildArtifacts.instance.xcode
557
+ destination_path = Illuminator::BuildArtifacts.instance.object_files
558
+
559
+ exclude_regex = '.*(Debug|contrib).*'
560
+ puts "Generating automation test coverage to #{destination_file}".green
561
+ sleep (3) # TODO: we are waiting for the app process to complete, maybe do this a different way
562
+
563
+ # cleanup
564
+ FileUtils.rm destination_file, :force => true
565
+
566
+ # we copy all the relevant build artifacts for coverage into a second folder. we may not need to do this.
567
+ file_paths = []
568
+ Find.find(xcode_artifacts_folder) do |pathP|
569
+ path = pathP.to_s
570
+ if /.*\.gcda$/.match path
571
+ file_paths << path
572
+ path_without_ext = path.chomp(File.extname(path))
573
+
574
+ file_paths << path_without_ext + '.d'
575
+ file_paths << path_without_ext + '.dia'
576
+ file_paths << path_without_ext + '.o'
577
+ file_paths << path_without_ext + '.gcno'
578
+ end
579
+ end
580
+
581
+ file_paths.each do |path|
582
+ FileUtils.cp path, destination_path
583
+ end
584
+
585
+ command = "gcovr -r '#{gcWorkspace}' --exclude='#{exclude_regex}' --xml '#{destination_path}' > '#{destination_file}'"
586
+ run_annotated_command(command)
587
+
588
+ end
589
+ end