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.
- checksums.yaml +7 -0
- data/gem/README.md +37 -0
- data/gem/bin/illuminatorTestRunner.rb +22 -0
- data/gem/lib/illuminator.rb +171 -0
- data/gem/lib/illuminator/argument-parsing.rb +299 -0
- data/gem/lib/illuminator/automation-builder.rb +39 -0
- data/gem/lib/illuminator/automation-runner.rb +589 -0
- data/gem/lib/illuminator/build-artifacts.rb +118 -0
- data/gem/lib/illuminator/device-installer.rb +45 -0
- data/gem/lib/illuminator/host-utils.rb +42 -0
- data/gem/lib/illuminator/instruments-runner.rb +301 -0
- data/gem/lib/illuminator/javascript-runner.rb +98 -0
- data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
- data/gem/lib/illuminator/listeners/full-output.rb +13 -0
- data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
- data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
- data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
- data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
- data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
- data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
- data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
- data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
- data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
- data/gem/lib/illuminator/options.rb +96 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
- data/gem/lib/illuminator/test-definitions.rb +23 -0
- data/gem/lib/illuminator/test-suite.rb +155 -0
- data/gem/lib/illuminator/version.rb +3 -0
- data/gem/lib/illuminator/xcode-builder.rb +144 -0
- data/gem/lib/illuminator/xcode-utils.rb +219 -0
- data/gem/resources/BuildConfiguration.xcconfig +10 -0
- data/gem/resources/js/AppMap.js +767 -0
- data/gem/resources/js/Automator.js +1132 -0
- data/gem/resources/js/Base64.js +142 -0
- data/gem/resources/js/Bridge.js +102 -0
- data/gem/resources/js/Config.js +92 -0
- data/gem/resources/js/Extensions.js +2025 -0
- data/gem/resources/js/Illuminator.js +228 -0
- data/gem/resources/js/Preferences.js +24 -0
- data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
- data/gem/resources/scripts/common.applescript +25 -0
- data/gem/resources/scripts/diff_png.sh +61 -0
- data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
- data/gem/resources/scripts/plist_to_json.sh +40 -0
- data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
- 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
|