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,1132 @@
1
+ // Automator.js
2
+ //
3
+ // creates 'automator' which can build and run scenarios
4
+
5
+ var debugAutomator = false;
6
+
7
+ (function () {
8
+
9
+ var root = this,
10
+ automator = null;
11
+
12
+ // put automator in namespace of importing code
13
+ if (typeof exports !== 'undefined') {
14
+ automator = exports;
15
+ } else {
16
+ automator = root.automator = {};
17
+ }
18
+
19
+ ////////////////////////////////////////////////////////////////////////////////////////////
20
+ //
21
+ // Exception classes and helpers
22
+ //
23
+ ////////////////////////////////////////////////////////////////////////////////////////////
24
+
25
+ automator.ScenarioSetupException = makeErrorClassWithGlobalLocator(__file__(), "ScenarioSetupException");
26
+
27
+
28
+ ////////////////////////////////////////////////////////////////////////////////////////////
29
+ //
30
+ // Callbacks for test initialization - customizing Illuminator's behavior
31
+ //
32
+ ////////////////////////////////////////////////////////////////////////////////////////////
33
+
34
+ // table of callbacks that are used by automator. sensible defaults.
35
+ automator.callback = {
36
+ onInit: function () { UIALogger.logDebug("Running default automator 'onInit' callback"); },
37
+ prepare: function () { UIALogger.logDebug("Running default automator 'prepare' callback"); },
38
+ preScenario: function (parm) { return UIALogger.logDebug("Returning true from default automator 'preScenario' callback " + JSON.stringify(parm)) || true; },
39
+ onScenarioPass: function (parm) { UIALogger.logDebug("Running default automator 'onScenarioPass' callback " + JSON.stringify(parm)); },
40
+ onScenarioFail: function (parm) { UIALogger.logDebug("Running default automator 'onScenarioFail' callback " + JSON.stringify(parm)); },
41
+ complete: function (parm) { UIALogger.logDebug("Running default automator 'complete' callback " + JSON.stringify(parm)); }
42
+ };
43
+
44
+ /**
45
+ * set the callback for Automator initialization, to be called only once -- after scenarios have been added
46
+ *
47
+ * The callback function takes an associative array with the following keys:
48
+ * - entryPoint
49
+ *
50
+ * @param fn the callback function, taking an associative array and whose return value is ignored
51
+ */
52
+ automator.setCallbackOnInit = function (fn) {
53
+ automator.callback["onInit"] = fn;
54
+ };
55
+
56
+ /**
57
+ * set the callback for Automator run preparation, to be called only once -- before any scenarios execute
58
+ *
59
+ * This callback function will only be called if the automator's entry point requires tests to be run
60
+ *
61
+ * @param fn the callback function, taking no arguments and whose return value is ignored
62
+ */
63
+ automator.setCallbackPrepare = function (fn) {
64
+ automator.callback["prepare"] = fn;
65
+ };
66
+
67
+ /**
68
+ * set the callback function for pre-scenario initialization -- called before each scenario run
69
+ *
70
+ * This callback function may return boolean false, indicating that the pre-scenario setup has failed.
71
+ * If the return value is false, Illuminator will restart Instruments (up to once per scenario) under
72
+ * the assumption that the application has reached a dead-end state and must be started fresh.
73
+ *
74
+ * @param fn the callback function, taking no arguments and returning false if setup was unsuccessful
75
+ */
76
+ automator.setCallbackPreScenario = function (fn) {
77
+ automator.callback["preScenario"] = fn;
78
+ };
79
+
80
+ /**
81
+ * set the callback function for successful completion of a scenario
82
+ *
83
+ * The callback function takes an associative array with the following keys:
84
+ * - scenarioName
85
+ * - timeStarted
86
+ * - duration
87
+ *
88
+ * @param fn the callback function, taking an associative array and whose return value is ignored
89
+ */
90
+ automator.setCallbackOnScenarioPass = function (fn) {
91
+ automator.callback["onScenarioPass"] = fn;
92
+ };
93
+
94
+ /**
95
+ * set the callback function for failed completion of a scenario
96
+ *
97
+ * The callback function takes an associative array with the following keys:
98
+ * - scenarioName
99
+ * - timeStarted
100
+ * - duration
101
+ *
102
+ * @param fn the callback function, taking an associative array and whose return value is ignored
103
+ */
104
+ automator.setCallbackOnScenarioFail = function (fn) {
105
+ automator.callback["onScenarioFail"] = fn;
106
+ };
107
+
108
+ /**
109
+ * set the callback function for the conclusion of all scenarios
110
+ *
111
+ * The callback function takes an associative array with the following keys:
112
+ * - timeStarted
113
+ * - duration
114
+ *
115
+ * @param fn the callback function, taking and whose return value is ignored
116
+ */
117
+ automator.setCallbackComplete = function (fn) {
118
+ automator.callback["complete"] = fn;
119
+ };
120
+
121
+
122
+ /**
123
+ * Safely execute a callback
124
+ *
125
+ * @param callbackName the string key into the callback array
126
+ * @param parameters the parameter array that should be passed to the callback
127
+ * @param doLogFail whether to log a failure message (i.e. whether we are currently in a test)
128
+ * @param doLogScreen whether to log the screen on a failure
129
+ * @return bool whether the callback was successful
130
+ */
131
+ automator._executeCallback = function (callbackName, parameters, doLogFail, doLogScreen) {
132
+ try {
133
+ // call with parameters if supplied and return normally
134
+ var ret;
135
+ if (parameters === undefined) {
136
+ ret = automator.callback[callbackName]();
137
+ } else {
138
+ ret = automator.callback[callbackName](parameters);
139
+ }
140
+
141
+ // special case for preScenario callback. TODO: make this less special-casey
142
+ if ("preScenario" == callbackName && false === ret) {
143
+ notifyIlluminatorFramework("Request instruments restart");
144
+ }
145
+
146
+ return true;
147
+ } catch (e) {
148
+ var failMessage = "Callback '" + callbackName + "' failed: " + e;
149
+
150
+ // log info as requested
151
+ if (doLogScreen) {
152
+ automator.logScreenInfo();
153
+ }
154
+ automator.logStackInfo(e);
155
+
156
+ if (doLogFail) {
157
+ UIALogger.logFail(failMessage);
158
+ } else {
159
+ UIALogger.logError(failMessage);
160
+ }
161
+ return false;
162
+ }
163
+ };
164
+
165
+
166
+ ////////////////////////////////////////////////////////////////////////////////////////////
167
+ //
168
+ // Functions to handle automator state -- ways for scenario steps to register side effects
169
+ //
170
+ ////////////////////////////////////////////////////////////////////////////////////////////
171
+ automator._state = {};
172
+ automator._state.external = {};
173
+
174
+ /**
175
+ * Reset the automator state for a new test scenario to run
176
+ */
177
+ automator._resetState = function () {
178
+ config.automatorModality = "reset";
179
+ automator._state.external = {};
180
+ automator._state.internal = {"deferredFailures": []};
181
+ };
182
+
183
+ /**
184
+ * Store a named state in automator
185
+ *
186
+ * @param key the name of the state
187
+ * @param value the value of the state
188
+ */
189
+ automator.setState = function (key, value) {
190
+ automator._state.external[key] = value;
191
+ };
192
+
193
+ /**
194
+ * Predicate, whether there is a stored state for a key
195
+ *
196
+ * @param key the key to check
197
+ * @return bool whether there is a state with that key
198
+ */
199
+ automator.hasState = function (key) {
200
+ return undefined !== automator._state.external[key];
201
+ };
202
+
203
+ /**
204
+ * Get the state with the given name. If it doesn't exist, return the default value
205
+ *
206
+ * @param key the name of the state
207
+ * @param defaultValue the value to return if key is undefined
208
+ */
209
+ automator.getState = function (key, defaultValue) {
210
+ if (automator.hasState(key)) return automator._state.external[key];
211
+
212
+ UIALogger.logDebug("Automator state '" + key + "' not found, returning default");
213
+ return defaultValue;
214
+ };
215
+
216
+ /**
217
+ * Defer a failure until the end of the test scenario
218
+ *
219
+ * @param err the error object
220
+ */
221
+ automator.deferFailure = function (err) {
222
+ UIALogger.logDebug("Deferring an error: " + err);
223
+ automator.logScreenInfo();
224
+ automator.logStackInfo(getStackTrace());
225
+
226
+ if (automator._state.internal["currentStepName"] && automator._state.internal["currentStepNumber"]) {
227
+ var msg = "Step " + automator._state.internal["currentStepNumber"];
228
+ msg += " (" + automator._state.internal["currentStepName"] + "): ";
229
+ automator._state.internal.deferredFailures.push(msg + err);
230
+ } else {
231
+ automator._state.internal.deferredFailures.push("<Undefined step>: " + err);
232
+ }
233
+ };
234
+
235
+
236
+ ////////////////////////////////////////////////////////////////////////////////////////////
237
+ //
238
+ // Functions to build test scenarios
239
+ //
240
+ ////////////////////////////////////////////////////////////////////////////////////////////
241
+
242
+ automator.allScenarios = []; // flat list of scenarios
243
+ automator.lastScenario = null; // state variable for building scenarios of steps
244
+ automator.allScenarioNames = {}; // for ensuring name uniqueness
245
+
246
+ // make a lookup array of characters that aren't allowed in tags
247
+ var disallowedTagChars = "!@#$%^&*()[]{}<>`~,'\"/\\+=;:";
248
+ automator.disallowedTagChars = {};
249
+ for (var i = 0; i < disallowedTagChars.length; ++i) {
250
+ automator.disallowedTagChars[disallowedTagChars[i]] = true;
251
+ }
252
+
253
+ /**
254
+ * Create an empty scenario with the given name and tags
255
+ *
256
+ * @param scenarioName the name for the scenario - must be unique
257
+ * @param tags array of tags for the scenario
258
+ * @return this
259
+ */
260
+ automator.createScenario = function (scenarioName, tags) {
261
+ if (tags === undefined) tags = ["_untagged"]; // always have a tag
262
+
263
+ // check uniqueness
264
+ if (automator.allScenarioNames[scenarioName]) {
265
+ throw new automator.ScenarioSetupException("Can't create Scenario '" + scenarioName + "', because that name already exists");
266
+ }
267
+ automator.allScenarioNames[scenarioName] = true;
268
+
269
+ // check for disallowed characters in tag names
270
+ for (var i = 0; i < tags.length; ++i) {
271
+ var tag = tags[i];
272
+ for (var j = 0; j < tag.length; ++j) {
273
+ c = tag[j];
274
+ if (automator.disallowedTagChars[c]) {
275
+ throw new automator.ScenarioSetupException("Disallowed character '" + c + "' in tag '" + tag + "' in scenario '" + scenarioName + "'");
276
+ }
277
+ }
278
+ }
279
+
280
+ // create base object
281
+ automator.lastScenario = {
282
+ title: scenarioName,
283
+ steps: []
284
+ };
285
+
286
+ if (debugAutomator) {
287
+ UIALogger.logDebug(["Automator creating scenario '", scenarioName, "'",
288
+ " [", tags.join(", "), "]",
289
+ ].join(""));
290
+ }
291
+
292
+ // add tags to objects
293
+ automator.lastScenario.tags_obj = {}; // convert tags to object
294
+ for (var i = 0; i < tags.length; ++i) {
295
+ var t = tags[i];
296
+ automator.lastScenario.tags_obj[t] = true;
297
+ }
298
+
299
+ // add information about where scenario was created (roughly)
300
+ var stack = getStackTrace();
301
+ for (var i = 0; i < stack.length; ++i) {
302
+ var l = stack[i];
303
+ if (!(l.nativeCode || l.file == "Automator.js")) {
304
+ automator.lastScenario.inFile = l.file;
305
+ automator.lastScenario.definedBy = l.functionName;
306
+ break;
307
+ }
308
+ }
309
+
310
+
311
+ // add new scenario to list
312
+ automator.allScenarios.push(automator.lastScenario);
313
+
314
+ return this;
315
+ };
316
+
317
+
318
+ /**
319
+ * Throw an exception if any parameters required for the screen action are not supplied
320
+ *
321
+ * @param screenAction an AppMap screen action
322
+ * @param suppliedParameters associative array of parameters
323
+ */
324
+ automator._assertAllRequiredParameters = function (screenAction, suppliedParameters) {
325
+ for (var ap in screenAction.params) {
326
+ if (screenAction.params[ap].required && (undefined === suppliedParameters || undefined === suppliedParameters[ap])) {
327
+ failmsg = ["In scenario '",
328
+ automator.lastScenario.title,
329
+ "' in step ", automator.lastScenario.steps.length + 1,
330
+ " (", screenAction.name, ") ",
331
+ "missing required parameter '",
332
+ ap,
333
+ "'; ",
334
+ automator.paramsToString(screenAction.params)
335
+ ].join("");
336
+ throw new automator.ScenarioSetupException(failmsg);
337
+ }
338
+ }
339
+ };
340
+
341
+
342
+ /**
343
+ * Throw an exception if any parameters supplied to the screen action are unrecognized
344
+ *
345
+ * @param screenAction an AppMap screen action
346
+ * @param suppliedParameters associative array of parameters
347
+ */
348
+ automator._assertAllKnownParameters = function (screenAction, suppliedParameters) {
349
+ for (var p in suppliedParameters) {
350
+ if (undefined === screenAction.params[p]) {
351
+ failmsg = ["In scenario '",
352
+ automator.lastScenario.title,
353
+ "' in step ", automator.lastScenario.steps.length + 1,
354
+ " (", screenAction.name, ") ",
355
+ "received undefined parameter '",
356
+ p,
357
+ "'; ",
358
+ automator.paramsToString(screenAction.params)
359
+ ].join("");
360
+ throw new automator.ScenarioSetupException(failmsg);
361
+ }
362
+ }
363
+ };
364
+
365
+
366
+ /**
367
+ * Add a step to the most recently created scenario
368
+ *
369
+ * @param screenAction an AppMap screen action
370
+ * @param desiredParameters associative array of parameters
371
+ * @return this
372
+ */
373
+ automator.withStep = function (screenAction, desiredParameters) {
374
+ // generate a helpful error message if the screen action isn't defined
375
+ if (undefined === screenAction || typeof screenAction === 'string') {
376
+ var failmsg = ["withStep received an undefined screen action in scenario '",
377
+ automator.lastScenario.title,
378
+ "'"
379
+ ];
380
+ var slength = automator.lastScenario.steps.length;
381
+ if (0 < slength) {
382
+ var goodAction = automator.lastScenario.steps[slength - 1].action;
383
+ failmsg.push(" after step " + goodAction.screenName + "." + goodAction.name);
384
+ }
385
+ throw new automator.ScenarioSetupException(failmsg.join(""));
386
+ }
387
+
388
+ // debug if necessary
389
+ if (debugAutomator) {
390
+ UIALogger.logDebug("screenAction is " + JSON.stringify(screenAction));
391
+ UIALogger.logDebug("screenAction.params is " + JSON.stringify(screenAction.params));
392
+ }
393
+
394
+ // create a step and check parameters
395
+ var step = {action: screenAction};
396
+ automator._assertAllRequiredParameters(screenAction, desiredParameters);
397
+ if (desiredParameters !== undefined) {
398
+ automator._assertAllKnownParameters(screenAction, desiredParameters);
399
+ step.parameters = desiredParameters;
400
+ }
401
+
402
+ // add step to scenario
403
+ automator.lastScenario.steps.push(step);
404
+ return this;
405
+ };
406
+
407
+
408
+ /**
409
+ * Add steps to the most recently created scenario by running a function that creates them
410
+ *
411
+ * @param stepGeneratorFn the function that will generate the steps
412
+ * @param desiredParameters associative array of parameters
413
+ * @return this
414
+ */
415
+ automator.withGeneratedSteps = function(stepGeneratorFn, desiredParameters) {
416
+ stepGeneratorFn(desiredParameters);
417
+ return this;
418
+ };
419
+
420
+ /**
421
+ * Add a step to the most recently created scenario if the given condition is true at scenario creation time
422
+ *
423
+ * @param screenAction an AppMap screen action
424
+ * @param desiredParameters associative array of parameters
425
+ * @return this
426
+ */
427
+ automator.withConditionalStep = function(condition, screenAction, desiredParameters) {
428
+ if(condition){
429
+ automator.withStep(screenAction, desiredParameters)
430
+ }
431
+ return this;
432
+ };
433
+
434
+ /**
435
+ * Add a repeated step to the most recently created scenario
436
+ *
437
+ * @param screenAction an AppMap screen action
438
+ * @param quantity the number of times that the step should be executed
439
+ * @param desiredParameters associative array of parameters, or a function taking 0-indexed run number that returns parameters
440
+ * @return this
441
+ */
442
+ automator.withRepeatedStep = function(screenAction, quantity, desiredParameters) {
443
+ var mkParm;
444
+
445
+ // use the function they made, or make a function that returns the params they supplied
446
+ if ((typeof desiredParameters) == "function") {
447
+ mkParm = desiredParameters;
448
+ } else {
449
+ mkParm = function (_) {
450
+ return desiredParameters;
451
+ };
452
+ }
453
+
454
+ for (var i = 0; i < quantity; ++i) {
455
+ automator.withStep(screenAction, mkParm(i));
456
+ }
457
+ return this;
458
+ };
459
+
460
+
461
+
462
+ ////////////////////////////////////////////////////////////////////////////////////////////
463
+ //
464
+ // Functions to run test scenarios
465
+ //
466
+ ////////////////////////////////////////////////////////////////////////////////////////////
467
+
468
+
469
+ automator.lastRunScenario = null;
470
+
471
+ /**
472
+ * ENTRY POINT: Run tagged scenarios
473
+ *
474
+ * Run scenarios that match 3 sets of provided tags
475
+ *
476
+ * @param tagsAny array - any scenario with any matching tag will run (if tags=[], run all)
477
+ * @param tagsAll array - any scenario with AT LEAST the same tags will run
478
+ * @param tagsNone array - any scenario with NONE of these tags will run
479
+ * @param randomSeed integer - if provided, will be used to randomize the run order
480
+ */
481
+ automator.runTaggedScenarios = function (tagsAny, tagsAll, tagsNone, randomSeed) {
482
+ UIALogger.logMessage("Automator running scenarios with tagsAny: [" + tagsAny.join(", ") + "]"
483
+ + ", tagsAll: [" + tagsAll.join(", ") + "]"
484
+ + ", tagsNone: [" + tagsNone.join(", ") + "]");
485
+
486
+ // filter the list by criteria
487
+ var onesToRun = [];
488
+ for (var i = 0; i < automator.allScenarios.length; ++i) {
489
+ var scenario = automator.allScenarios[i];
490
+ if (automator.scenarioMatchesCriteria(scenario, tagsAny, tagsAll, tagsNone)
491
+ && automator.targetSupportsScenario(scenario)) {
492
+ onesToRun.push(scenario);
493
+ }
494
+ }
495
+
496
+ automator.runScenarioList(onesToRun, randomSeed);
497
+ };
498
+
499
+ /**
500
+ * ENTRY POINT: Run named scenarios
501
+ *
502
+ * Run scenarios that match the names of those provided
503
+ *
504
+ * @param scenarioNames array - the list of named scenarios to run
505
+ * @param randomSeed integer - if provided, will be used to randomize the run order
506
+ */
507
+ automator.runNamedScenarios = function (scenarioNames, randomSeed) {
508
+ UIALogger.logMessage("Automator running " + scenarioNames.length + " scenarios by name");
509
+
510
+ // filter the list by name
511
+ var onesToRun = [];
512
+ // consider the full list of scenarios
513
+ for (var i = 0; i < automator.allScenarios.length; ++i) {
514
+ var scenario = automator.allScenarios[i];
515
+ // check whether any of the given scenario names match the scenario in the master list
516
+ for (var j = 0; j < scenarioNames.length; ++j) {
517
+ if (scenario.title == scenarioNames[j] && automator.targetSupportsScenario(scenario)) {
518
+ onesToRun.push(scenario);
519
+ }
520
+ }
521
+ }
522
+
523
+ automator.runScenarioList(onesToRun, randomSeed);
524
+ };
525
+
526
+
527
+ /**
528
+ * run a given list of scenarios, optionally in randomized order
529
+ *
530
+ * @param senarioList array of scenarios to run, in order
531
+ * @param ramdomSeed optional number, if provided the test run order will be randomized with this as a seed
532
+ */
533
+ automator.runScenarioList = function (scenarioList, randomSeed) {
534
+ // randomize if asked
535
+ if (randomSeed !== undefined) {
536
+ UIALogger.logMessage("Automator RANDOMIZING scenarios with seed = " + randomSeed);
537
+ onesToRun = automator.shuffle(scenarioList, randomSeed);
538
+ }
539
+
540
+ // run initial callback and only continue on if it succeeds
541
+ if (!automator._executeCallback("prepare", undefined, false, false)) {
542
+ notifyIlluminatorFramework("Successful launch");
543
+ UIALogger.logMessage("Automator's 'prepare' callback failed, so halting.");
544
+ return;
545
+ }
546
+
547
+ // At this point, we consider the instruments/app launch to be a success
548
+ // this function will also serve as notification to the framework that we consider instruments to have started
549
+ automator.saveIntendedTestList(scenarioList);
550
+ var offset = config.automatorScenarioOffset;
551
+
552
+ var dt;
553
+ var t0 = getTime();
554
+ // iterate through scenarios and run them
555
+ UIALogger.logMessage(scenarioList.length + " scenarios to run");
556
+ for (var i = 0; i < scenarioList.length; i++) {
557
+ var message = "Running scenario " + (i + 1 + offset).toString() + " of " + (scenarioList.length + offset);
558
+ automator.runScenario(scenarioList[i], message);
559
+ }
560
+ dt = getTime() - t0;
561
+ UIALogger.logMessage("Completed running scenario list ("
562
+ + scenarioList.length + " of "
563
+ + (scenarioList.length + offset) + " total scenarios) "
564
+ + " in " + secondsToHMS(dt));
565
+
566
+ // create a CSV report for the amount of time spent evaluating selectors
567
+ automator.saveSelectorReportCSV("selectorTimeCostReport");
568
+
569
+ // run completion callback
570
+ var info = {
571
+ scenarioCount: scenarioList.length,
572
+ timeStarted: t0,
573
+ duration: dt
574
+ };
575
+ automator._executeCallback("complete", info, false, false);
576
+
577
+ return this;
578
+ };
579
+
580
+
581
+ /**
582
+ * Save a JSON structure indicating the list of tests that will be run
583
+ *
584
+ * @param scenarioList an array of scenario objects
585
+ */
586
+ automator.saveIntendedTestList = function (scenarioList) {
587
+ var names = [];
588
+ for (var i = 0; i < scenarioList.length; ++i) {
589
+ names.push(scenarioList[i].title);
590
+ }
591
+
592
+ var intendedListPath = config.buildArtifacts.intendedTestList;
593
+ if (!host().writeToFile(intendedListPath, JSON.stringify({scenarioNames: names}, null, " "))) {
594
+ throw new IlluminatorRuntimeFailureException("Could not save intended test list to " + intendedListPath);
595
+ }
596
+
597
+ notifyIlluminatorFramework("Saved intended test list to: " + intendedListPath);
598
+ };
599
+
600
+ /**
601
+ * Save a report to disk of the amount of time evaluating selectors (CSV)
602
+ *
603
+ * @param selectorReport the value of extensionProfiler.getCriteriaCost()
604
+ * @param reportName the basename of the report -- no path, no .csv extension
605
+ */
606
+ automator.saveSelectorReportCSV = function (reportName) {
607
+ var totalSelectorTime = 0;
608
+ var selectorReportCsvPath = config.buildArtifacts.root + "/" + reportName + ".csv";
609
+ var csvLines = ["\"Total time (seconds)\",Count,\"Average time\",Selector"];
610
+ var selectorReport = extensionProfiler.getCriteriaCost();
611
+ for (var i = 0; i < selectorReport.length; ++i) {
612
+ var rec = selectorReport[i];
613
+ totalSelectorTime += rec.time;
614
+ csvLines.push(rec.time.toString() + "," + rec.hits + "," + (rec.time / rec.hits) + ",\"" + rec.criteria.replace(/"/g, '""') + '"');
615
+ }
616
+ if (host().writeToFile(selectorReportCsvPath, csvLines.join("\n"))) {
617
+ UIALogger.logMessage("Overall time spent evaluating soft selectors: " + secondsToHMS(totalSelectorTime)
618
+ + " - full report at " + selectorReportCsvPath);
619
+ }
620
+ };
621
+
622
+ /**
623
+ * Run a single scenario and handle all its reporting callbacks
624
+ *
625
+ * @param scenario an automator scenario
626
+ * @param message string a message to print at the beginning of the test, immediately after the start
627
+ */
628
+ automator.runScenario = function (scenario, message) {
629
+ var t1 = getTime();
630
+ var passed = automator._evaluateScenario(scenario, message);
631
+ var dt = getTime() - t1;
632
+ var info = {
633
+ scenarioName: scenario.title,
634
+ scenarioTags: Object.keys(scenario.tags_obj),
635
+ timeStarted: t1,
636
+ duration: dt
637
+ };
638
+
639
+ UIALogger.logDebug("Scenario completed in " + secondsToHMS(dt));
640
+ automator._executeCallback(passed ? "onScenarioPass" : "onScenarioFail", info, false, false);
641
+ };
642
+
643
+
644
+ /**
645
+ * Describe a scenario step (to the log)
646
+ *
647
+ * @param stepNumber the 1-indexed number of this step in the scenario
648
+ * @param totalSteps the total number of steps in this scenario
649
+ * @param step an automator scenario step
650
+ */
651
+ automator._logScenarioStep = function (stepNumber, totalSteps, step) {
652
+ // build the parameter list to go in the step description
653
+ var parameters = step.parameters;
654
+ var parameters_arr = [];
655
+ var parameters_str = "";
656
+ for (var k in parameters) {
657
+ var v = parameters[k];
658
+ if (step.action.params[k].useInSummary && undefined !== v) {
659
+ parameters_arr.push(k.toString() + ": " + v.toString());
660
+ }
661
+ }
662
+
663
+ // make the descriptive parameter string
664
+ parameters_str = parameters_arr.length ? (" {" + parameters_arr.join(", ") + "}") : "";
665
+
666
+ // build the step description
667
+ UIALogger.logMessage(["STEP ", stepNumber, " of ", totalSteps, ": ",
668
+ "(", step.action.appName, ".", step.action.screenName, ".", step.action.name, ") ",
669
+ step.action.description,
670
+ parameters_str
671
+ ].join(""));
672
+ };
673
+
674
+
675
+ /**
676
+ * Assert that an automator step is on the correct screen
677
+ *
678
+ * @param step an automator scenario step
679
+ */
680
+ automator._assertCorrectScreen = function (step) {
681
+ // assert isCorrectScreen function exists
682
+ if (undefined === step.action.isCorrectScreen[config.implementation]) {
683
+ throw new IlluminatorSetupException(["No isCorrectScreen function defined for '",
684
+ step.action.screenName, ".", step.action.name,
685
+ "' on ", config.implementation].join(""));
686
+ }
687
+
688
+ // assert correct screen
689
+ if (!step.action.isCorrectScreen[config.implementation]()) {
690
+ throw new IlluminatorRuntimeVerificationException(["Failed assertion that '", step.action.screenName, "' is active"].join(""));
691
+ }
692
+ };
693
+
694
+
695
+ /**
696
+ * Extract the implementation-specific action from a step and execute it
697
+ *
698
+ * @param step an automator scenario step
699
+ */
700
+ automator._executeStepAction = function (step) {
701
+ var actFn = step.action.actionFn["default"];
702
+ if (step.action.actionFn[config.implementation] !== undefined) actFn = step.action.actionFn[config.implementation];
703
+
704
+ // call step action with or without parameters, as appropriate
705
+ if (step.parameters !== undefined) {
706
+ actFn.call(this, step.parameters);
707
+ } else if (0 < Object.keys(step.action.params).length) {
708
+ actFn.call(this, {}); // ensure param'd functions always receive an argument
709
+ } else {
710
+ actFn.call(this);
711
+ }
712
+ };
713
+
714
+
715
+ /**
716
+ * Run a single scenario and return its pass/fail status
717
+ *
718
+ * @param scenario an automator scenario
719
+ * @param message string a message to print at the beginning of the test, immediately after the start
720
+ * @return boolean whether the scenario finished successfully
721
+ */
722
+ automator._evaluateScenario = function (scenario, message) {
723
+
724
+ config.automatorModality = "initScenario";
725
+ var testname = scenario.title;
726
+ UIALogger.logDebug("###############################################################");
727
+ UIALogger.logStart(testname);
728
+ UIALogger.logMessage(["Scenario tags are [", Object.keys(scenario.tags_obj).join(", "), "]"].join(""));
729
+ if (undefined !== message) {
730
+ UIALogger.logMessage(message);
731
+ }
732
+
733
+ // print the previous scenario in case we are running with a randomizer
734
+ if (automator.lastRunScenario) {
735
+ UIALogger.logMessage("(Previous test was: " + automator.lastRunScenario + ")");
736
+ } else {
737
+ UIALogger.logDebug("(No previous test)");
738
+ }
739
+ automator.lastRunScenario = scenario.title;
740
+
741
+ // initialize the scenario
742
+ UIALogger.logDebug("----------------------------------------------------------------");
743
+ UIALogger.logMessage("STEP 0: Reset automator for new scenario");
744
+ automator._resetState();
745
+ if (!automator._executeCallback("preScenario",
746
+ {scenarioName: scenario.title, scenarioTags: Object.keys(scenario.tags_obj)},
747
+ true, true)) {
748
+ return false;
749
+ }
750
+
751
+ // wrap the iteration of the test steps in try/catch
752
+ var step = null;
753
+ try {
754
+ config.automatorModality = "executeScenario";
755
+
756
+ // if we iterate all steps without exception, test passes
757
+ for (var i = 0; i < scenario.steps.length; i++) {
758
+ var step = scenario.steps[i];
759
+ if (debugAutomator) {
760
+ UIALogger.logDebug(["DEBUG step ", i + 1, JSON.stringify(step)].join(""));
761
+ }
762
+
763
+ // set the current step name
764
+ automator._state.internal["currentStepName"] = step.action.screenName + "." + step.action.name;
765
+ automator._state.internal["currentStepNumber"] = i + 1;
766
+
767
+ // log this step to the console
768
+ UIALogger.logDebug("----------------------------------------------------------------");
769
+ automator._logScenarioStep(i + 1, scenario.steps.length, step);
770
+
771
+ // make sure the screen containing the action is active
772
+ automator._assertCorrectScreen(step);
773
+
774
+ // retrieve and execute the correct step action
775
+ automator._executeStepAction(step);
776
+
777
+ }
778
+
779
+ // check for any deferred errors
780
+ if (0 < automator._state.internal.deferredFailures.length) {
781
+ for (var i = 0; i < automator._state.internal.deferredFailures.length; ++i) {
782
+ UIALogger.logMessage("Deferred Failure " + (i + 1).toString() + ": " + automator._state.internal.deferredFailures[i]);
783
+ }
784
+ UIALogger.logFail(["The test completed all its steps, but",
785
+ automator._state.internal.deferredFailures.length.toString(),
786
+ "failures were deferred"].join(" "));
787
+ return false;
788
+ }
789
+
790
+ } catch (exception) {
791
+ config.automatorModality = "handleException";
792
+ var failmsg = exception.message ? exception.message : exception.toString();
793
+ var longmsg = (['Step ', i + 1, " of ", scenario.steps.length, " (",
794
+ step.action.screenName, ".", step.action.name,
795
+ ') failed in scenario: "', scenario.title,
796
+ '" with message: ', failmsg].join(""));
797
+
798
+ UIALogger.logDebug("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
799
+ UIALogger.logDebug(["FAILED:", failmsg].join(" "));
800
+ notifyIlluminatorFramework("Stack trace follows:");
801
+ automator.logScreenInfo();
802
+ automator.logStackInfo(exception);
803
+ UIATarget.localTarget().captureScreenWithName(step.name);
804
+ UIALogger.logDebug("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
805
+
806
+ // check for any deferred errors
807
+ if (0 < automator._state.internal.deferredFailures.length) {
808
+ for (var i = 0; i < automator._state.internal.deferredFailures.length; ++i) {
809
+ UIALogger.logMessage("Deferred Failure " + (i + 1).toString() + ": " + automator._state.internal.deferredFailures[i]);
810
+ }
811
+
812
+ longmsg += [" ::", automator._state.internal.deferredFailures.length.toString(),
813
+ "other failures had been deferred"].join(" ");
814
+ }
815
+ UIALogger.logDebug(longmsg);
816
+ UIALogger.logFail(longmsg);
817
+ return false;
818
+ }
819
+
820
+ UIALogger.logPass(testname);
821
+ return true;
822
+ };
823
+
824
+
825
+ /**
826
+ * whether a given scenario is supported by the desired target implementation
827
+ *
828
+ * @param scenario an automator scenario
829
+ * @return bool
830
+ */
831
+ automator.targetSupportsScenario = function (scenario) {
832
+ // if any actions are neither defined for the current target nor "default"
833
+ for (var i = 0; i < scenario.steps.length; ++i) {
834
+ var s = scenario.steps[i];
835
+ // target not defined
836
+ if (undefined === s.action.isCorrectScreen[config.implementation]) {
837
+ UIALogger.logDebug(["Skipping scenario '", scenario.title,
838
+ "' because screen '", s.action.screenName, "'",
839
+ " doesn't have a screenIsActive function for ", config.implementation].join(""));
840
+ return false;
841
+ }
842
+
843
+ // action not defined for target
844
+ if (s.action.actionFn["default"] === undefined && s.action.actionFn[config.implementation] === undefined) {
845
+ UIALogger.logDebug(["Skipping scenario '", scenario.title, "' because action '",
846
+ s.action.screenName, ".", s.action.name,
847
+ "' isn't suppored on ", config.implementation].join(""));
848
+ return false;
849
+ }
850
+ }
851
+
852
+ return true;
853
+ };
854
+
855
+
856
+ /**
857
+ * Whether a given scenario is a match for the given tags
858
+ *
859
+ * @param scenario an automator scenario
860
+ * @param tagsAny array - any scenario with any matching tag will run (if tags=[], run all)
861
+ * @param tagsAll array - any scenario with AT LEAST the same tags will run
862
+ * @param tagsNone array - any scenario with NONE of these tags will run
863
+ * @return bool
864
+ */
865
+ automator.scenarioMatchesCriteria = function (scenario, tagsAny, tagsAll, tagsNone) {
866
+ // if any tagsAll are missing from scenario, fail
867
+ for (var i = 0; i < tagsAll.length; ++i) {
868
+ var t = tagsAll[i];
869
+ if (!(t in scenario.tags_obj)) return false;
870
+ }
871
+
872
+ // if any tagsNone are present in scenario, fail
873
+ for (var i = 0; i < tagsNone.length; ++i) {
874
+ var t = tagsNone[i];
875
+ if (t in scenario.tags_obj) return false;
876
+ }
877
+
878
+ // if no tagsAny specified, special case for ALL tags
879
+ if (0 == tagsAny.length) return true;
880
+
881
+ // if any tagsAny are present in scenario, pass
882
+ for (var i = 0; i < tagsAny.length; ++i) {
883
+ var t = tagsAny[i];
884
+ if (t in scenario.tags_obj) return true;
885
+ }
886
+
887
+ return false; // no tags matched
888
+ };
889
+
890
+
891
+ /**
892
+ * Shuffle an array - Knuth Shuffle implementation using a PRNG
893
+ *
894
+ * @param array the array to be shuffled
895
+ * @param seed number to seed the PRNG
896
+ */
897
+ automator.shuffle = function (array, seed) {
898
+ var idx = array.length;
899
+ var tmp;
900
+ var rnd;
901
+
902
+ // count backwards from the end of the array, swapping the current element with a random one
903
+ while (0 !== idx) {
904
+ // randomize BEFORE decrement because we get a modded value
905
+ rnd = (Math.pow(2147483647, idx) + seed) % array.length; // use merseinne prime
906
+ idx -= 1;
907
+
908
+ // swap
909
+ tmp = array[idx];
910
+ array[idx] = array[rnd];
911
+ array[rnd] = tmp;
912
+ }
913
+
914
+ return array;
915
+ };
916
+
917
+
918
+ ////////////////////////////////////////////////////////////////////////////////////////////
919
+ //
920
+ // Functions to describe the Automator
921
+ //
922
+ ////////////////////////////////////////////////////////////////////////////////////////////
923
+
924
+ /**
925
+ * generate a readable description of the parameters that an action expects
926
+ *
927
+ * @param actionParams an associative array of parameters that an action defines
928
+ * @return string
929
+ */
930
+ automator.paramsToString = function (actionParams) {
931
+ var param_list = [];
932
+ for (var p in actionParams) {
933
+ var pp = actionParams[p];
934
+ param_list.push([p,
935
+ " (",
936
+ pp.required ? "required" : "optional",
937
+ ": ",
938
+ pp.description,
939
+ ")"
940
+ ].join(""));
941
+ }
942
+
943
+ return ["parameters are: [",
944
+ param_list.join(", "),
945
+ "]"].join("");
946
+ };
947
+
948
+
949
+ /**
950
+ * log some information about the automation environment
951
+ */
952
+ automator.logInfo = function () {
953
+ UIALogger.logMessage("Target info: " +
954
+ "name='" + target().name() + "', " +
955
+ "model='" + target().model() + "', " +
956
+ "systemName='" + target().systemName() + "', " +
957
+ "systemVersion='" + target().systemVersion() + "', ");
958
+
959
+ var tags = {};
960
+ for (var s = 0; s < automator.allScenarios.length; ++s) {
961
+ var scenario = automator.allScenarios[s];
962
+
963
+ // get all tags
964
+ for (var k in scenario.tags_obj) {
965
+ tags[k] = 1;
966
+ }
967
+ }
968
+
969
+ var tagsArr = [];
970
+ for (var k in tags) {
971
+ tagsArr.push(k);
972
+ }
973
+
974
+ UIALogger.logMessage("Defined tags: '" + tagsArr.join("', '") + "'");
975
+
976
+ };
977
+
978
+
979
+ /**
980
+ * Log information about the currently-shown iOS screen
981
+ *
982
+ */
983
+ automator.logScreenInfo = function () {
984
+ //UIATarget.localTarget().logElementTree(); // ugly
985
+ UIALogger.logDebug(target().elementReferenceDump("target()"));
986
+ UIALogger.logDebug(target().elementReferenceDump("target()", true));
987
+ };
988
+
989
+ /**
990
+ * Log information about the current stack
991
+ *
992
+ * @param mixed either an error object or a stack array
993
+ */
994
+ automator.logStackInfo = function (mixed) {
995
+ var stack;
996
+
997
+ if (mixed instanceof Array) {
998
+ stack = mixed;
999
+ } else {
1000
+ var decoded = decodeStackTrace(mixed);
1001
+
1002
+ if (!decoded.isOK) {
1003
+ UIALogger.logMessage("Decoding stack trace didn't work: " + decoded.message);
1004
+ } else {
1005
+ UIALogger.logMessage("Stack trace from " + decoded.errorName + ":");
1006
+ }
1007
+ stack = decoded.stack;
1008
+ }
1009
+
1010
+ for (var i = 0; i < stack.length; ++i) {
1011
+ var l = stack[i];
1012
+ var position = " #" + i + ": ";
1013
+ var funcName = l.functionName === undefined ? "(anonymous)" : l.functionName;
1014
+ if (l.nativeCode) {
1015
+ UIALogger.logMessage(position + funcName + " from native code");
1016
+ } else {
1017
+ UIALogger.logMessage(position + funcName + " at " + l.file + " line " + l.line + " col " + l.column);
1018
+ }
1019
+ }
1020
+ };
1021
+
1022
+
1023
+ /**
1024
+ * Render the automator scenarios (and their steps, and parameters) to markdown
1025
+ *
1026
+ * @return string containing markdown
1027
+ */
1028
+ automator.toMarkdown = function () {
1029
+ var ret = ["The following scenarios are defined in the Illuminator Automator:"];
1030
+
1031
+ var title = function (rank, text) {
1032
+ var total = 4;
1033
+ for (var i = 0; i <= (total - rank); ++i) {
1034
+ ret.push("");
1035
+ }
1036
+
1037
+ switch (rank) {
1038
+ case 1:
1039
+ ret.push(text);
1040
+ ret.push(Array(Math.max(10, text.length) + 1).join("="));
1041
+ break;
1042
+ case 2:
1043
+ ret.push(text);
1044
+ ret.push(Array(Math.max(10, text.length) + 1).join("-"));
1045
+ break;
1046
+ default:
1047
+ ret.push(Array(rank + 1).join("#") + " " + text);
1048
+ }
1049
+ };
1050
+
1051
+ title(1, "Automator Scenarios");
1052
+ // iterate over scenarios
1053
+ for (var i = 0; i < automator.allScenarios.length; ++i) {
1054
+ var scenario = automator.allScenarios[i];
1055
+ title(2, scenario.title);
1056
+ ret.push("Tags: `" + Object.keys(scenario.tags_obj).join("`, `") + "`");
1057
+ ret.push("");
1058
+
1059
+ // iterate over steps (actions)
1060
+ for (var j = 0; j < scenario.steps.length; ++j) {
1061
+ var step = scenario.steps[j];
1062
+ ret.push((j + 1).toString() + ". **" + step.action.screenName + "." + step.action.name + "**: " + step.action.description);
1063
+
1064
+ // iterate over parameters in the action
1065
+ for (var k in step.parameters) {
1066
+ var val = step.parameters[k];
1067
+ var v;
1068
+
1069
+ // formatting based on datatype of parameter
1070
+ switch ((typeof val).toString()) {
1071
+ case "number":
1072
+ case "boolean":
1073
+ v = val; // no change
1074
+ break;
1075
+ case "function":
1076
+ v = "\n\n```javascript\n" + val + "\n```";
1077
+ break;
1078
+ case "string":
1079
+ v = "`" + val + "`"; // backtick-quote
1080
+ break;
1081
+ default:
1082
+ v = "`" + JSON.stringify(val) + "`"; // stringify and annotate with type
1083
+ if (val instanceof Array) {
1084
+ v += " (Array)";
1085
+ } else {
1086
+ v += " (" + (typeof val) + ")";
1087
+ }
1088
+ }
1089
+ ret.push(" * `" + k + "` = " + v);
1090
+ }
1091
+ }
1092
+ }
1093
+ return ret.join("\n");
1094
+ };
1095
+
1096
+
1097
+ /**
1098
+ * Render the automator scenarios (tags and steps) to a javascript object
1099
+ *
1100
+ * @param includeSteps whether to include the list of test steps in the output
1101
+ * @return object
1102
+ */
1103
+ automator.toScenarioObject = function (includeSteps) {
1104
+ var ret = {scenarios: []};
1105
+
1106
+ // iterate over scenarios
1107
+ for (var i = 0; i < automator.allScenarios.length; ++i) {
1108
+ var scenario = automator.allScenarios[i];
1109
+ var outScenario = {
1110
+ title: scenario.title,
1111
+ tags: Object.keys(scenario.tags_obj),
1112
+ inFile: scenario.inFile,
1113
+ definedBy: scenario.definedBy,
1114
+ };
1115
+
1116
+ if (includeSteps) {
1117
+ outScenario.steps = [];
1118
+
1119
+ // iterate over steps (actions)
1120
+ for (var j = 0; j < scenario.steps.length; ++j) {
1121
+ var step = scenario.steps[j];
1122
+ outScenario.steps.push(step.action.screenName + "." + step.action.name);
1123
+ }
1124
+ }
1125
+ ret.scenarios.push(outScenario);
1126
+ }
1127
+
1128
+ return ret;
1129
+ };
1130
+
1131
+
1132
+ }).call(this);