illuminator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/gem/README.md +37 -0
  3. data/gem/bin/illuminatorTestRunner.rb +22 -0
  4. data/gem/lib/illuminator.rb +171 -0
  5. data/gem/lib/illuminator/argument-parsing.rb +299 -0
  6. data/gem/lib/illuminator/automation-builder.rb +39 -0
  7. data/gem/lib/illuminator/automation-runner.rb +589 -0
  8. data/gem/lib/illuminator/build-artifacts.rb +118 -0
  9. data/gem/lib/illuminator/device-installer.rb +45 -0
  10. data/gem/lib/illuminator/host-utils.rb +42 -0
  11. data/gem/lib/illuminator/instruments-runner.rb +301 -0
  12. data/gem/lib/illuminator/javascript-runner.rb +98 -0
  13. data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
  14. data/gem/lib/illuminator/listeners/full-output.rb +13 -0
  15. data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
  16. data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
  17. data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
  18. data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
  19. data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
  20. data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
  21. data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
  22. data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
  23. data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
  24. data/gem/lib/illuminator/options.rb +96 -0
  25. data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
  26. data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
  27. data/gem/lib/illuminator/test-definitions.rb +23 -0
  28. data/gem/lib/illuminator/test-suite.rb +155 -0
  29. data/gem/lib/illuminator/version.rb +3 -0
  30. data/gem/lib/illuminator/xcode-builder.rb +144 -0
  31. data/gem/lib/illuminator/xcode-utils.rb +219 -0
  32. data/gem/resources/BuildConfiguration.xcconfig +10 -0
  33. data/gem/resources/js/AppMap.js +767 -0
  34. data/gem/resources/js/Automator.js +1132 -0
  35. data/gem/resources/js/Base64.js +142 -0
  36. data/gem/resources/js/Bridge.js +102 -0
  37. data/gem/resources/js/Config.js +92 -0
  38. data/gem/resources/js/Extensions.js +2025 -0
  39. data/gem/resources/js/Illuminator.js +228 -0
  40. data/gem/resources/js/Preferences.js +24 -0
  41. data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
  42. data/gem/resources/scripts/common.applescript +25 -0
  43. data/gem/resources/scripts/diff_png.sh +61 -0
  44. data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
  45. data/gem/resources/scripts/plist_to_json.sh +40 -0
  46. data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
  47. metadata +225 -0
@@ -0,0 +1,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);