illuminator 0.1.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.
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