illuminator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|