smart_monkey 0.1

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +17 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +77 -0
  6. data/Rakefile +57 -0
  7. data/Troubleshooting.md +61 -0
  8. data/VERSION +1 -0
  9. data/bin/smart_monkey +53 -0
  10. data/lib/bootstrap/css/bootstrap-responsive.css +1109 -0
  11. data/lib/bootstrap/css/bootstrap-responsive.min.css +9 -0
  12. data/lib/bootstrap/css/bootstrap.css +6167 -0
  13. data/lib/bootstrap/css/bootstrap.min.css +9 -0
  14. data/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
  15. data/lib/bootstrap/img/glyphicons-halflings.png +0 -0
  16. data/lib/bootstrap/js/bootstrap.js +2280 -0
  17. data/lib/bootstrap/js/bootstrap.min.js +6 -0
  18. data/lib/ios_device_log/deviceconsole +0 -0
  19. data/lib/smart_monkey.rb +2 -0
  20. data/lib/smart_monkey/command_helper.rb +71 -0
  21. data/lib/smart_monkey/monkey_runner.rb +549 -0
  22. data/lib/smart_monkey/templates/automation_result.xsl +61 -0
  23. data/lib/smart_monkey/templates/index.html.erb +77 -0
  24. data/lib/smart_monkey/templates/result.html.erb +110 -0
  25. data/lib/smart_monkey/templates/result_view.coffee +160 -0
  26. data/lib/smart_monkey/templates/result_view.js +250 -0
  27. data/lib/ui-auto-monkey/UIAutoMonkey.js +470 -0
  28. data/lib/ui-auto-monkey/custom.js +73 -0
  29. data/lib/ui-auto-monkey/handler/buttonHandler.js +111 -0
  30. data/lib/ui-auto-monkey/handler/wbScrollViewButtonHandler.js +114 -0
  31. data/lib/ui-auto-monkey/tuneup/LICENSE +20 -0
  32. data/lib/ui-auto-monkey/tuneup/assertions.js +402 -0
  33. data/lib/ui-auto-monkey/tuneup/image_asserter +26 -0
  34. data/lib/ui-auto-monkey/tuneup/image_assertion.js +65 -0
  35. data/lib/ui-auto-monkey/tuneup/image_assertion.rb +102 -0
  36. data/lib/ui-auto-monkey/tuneup/lang-ext.js +76 -0
  37. data/lib/ui-auto-monkey/tuneup/screen.js +11 -0
  38. data/lib/ui-auto-monkey/tuneup/test.js +71 -0
  39. data/lib/ui-auto-monkey/tuneup/test_runner/abbreviated_console_output.rb +38 -0
  40. data/lib/ui-auto-monkey/tuneup/test_runner/colored_console_output.rb +27 -0
  41. data/lib/ui-auto-monkey/tuneup/test_runner/console_output.rb +17 -0
  42. data/lib/ui-auto-monkey/tuneup/test_runner/preprocessor.rb +25 -0
  43. data/lib/ui-auto-monkey/tuneup/test_runner/run +343 -0
  44. data/lib/ui-auto-monkey/tuneup/test_runner/xunit_output.rb +114 -0
  45. data/lib/ui-auto-monkey/tuneup/tuneup.js +6 -0
  46. data/lib/ui-auto-monkey/tuneup/tuneup_js.podspec +52 -0
  47. data/lib/ui-auto-monkey/tuneup/uiautomation-ext.js +965 -0
  48. data/smart_monkey.gemspec +112 -0
  49. data/spec/spec_helper.rb +12 -0
  50. metadata +192 -0
@@ -0,0 +1,470 @@
1
+ // Copyright (c) 2013 Jonathan Penn (http://cocoamanifest.net/)
2
+
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+
10
+ // The above copyright notice and this permission notice shall be included in
11
+ // all copies or substantial portions of the Software.
12
+
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ // THE SOFTWARE.
20
+
21
+ "use strict";
22
+
23
+ function UIAutoMonkey() {
24
+
25
+
26
+ this.config = {
27
+ //run either by minutesToRun or numberOfEvents. Only one of these can set. (To use minutes you can use config.numberOfEvents = 0)
28
+ //minutesToRun = 60 * 8; //sample to run for 8 hours.
29
+ //checkTimeEvery = 60; //how often to check (in events) if minutesToRun has is used.
30
+ numberOfEvents: 100000,
31
+ delayBetweenEvents: 0.05, // In seconds
32
+
33
+ /**
34
+ * Sometimes the monkey can fall into UI Holes from which it it is hard to escape. The monkey may then spend an inordinate
35
+ * amount of time in such holes, neglecting other parts of the application.
36
+ *
37
+ * For example, if a parent Window P has a large image
38
+ * and clicking on the image opens a child window C from which one exits by tapping a small X on the top right, then until that small X is
39
+ * tapped we will reamin in C. conditionHandlers offer the developer the option to periodically recognize that we are in C and press the X.
40
+ *
41
+ * See buttonHandler.js for a specialized conditionHandler useful when a top level button can be used to escape from a UI hole.
42
+ *
43
+ * conditionHandlers are objects that respond to the following methods:
44
+ * isTrue(target, eventNumber): returns True if the condition is true given target and event number eventNumber.
45
+ * checkEvery(): How many events should pass before we check.
46
+ * handle(target, mainWindow) handle the condition.
47
+ * isExclusive() if true then if this condition's handler is invoked then processing subsequent conditions is skipped for this particular event. This
48
+ * is usually set to true as it allows the condition to exit a UI hole and at that point there may be no point executing other conditions
49
+ * logStats() log statics using UIALogger;
50
+ * condition handers must have the following property
51
+ * statsHandleInvokedCount - the count of the number of times we were invoked
52
+ */
53
+
54
+ conditionHandlers: [],
55
+
56
+ /**
57
+ * Unfortunately if the application is not responsive "ANR", the monkey may not notice and continue to fire events not realizing that
58
+ * the application is stuck. When run via continuous integration users may not notice that "successful" monkey runs in fact were in an
59
+ * ANR state.
60
+ *
61
+ * To deal with this the monkey supports ANR detection. Using an anrFingerprint function it periodically takes a fingerprint and if these
62
+ * are identical for a specificed interval then an ANR exception is thrown.
63
+ *
64
+ *
65
+ */
66
+ anrSettings: {
67
+ //fingerprintFunction defaults to false which will disable ANR fingerprinting. Otherwise set to a function that will return
68
+ //a string. One useful idiom using tuneup.js is
69
+ //#import tuneup.js
70
+ //config.anrSettings.fingerprintFunction = function() {return logVisibleElementTreeJSON(false)};
71
+ fingerprintFunction: false,
72
+ eventsBeforeANRDeclared: 1500, //throw exception if the fingerprint hasn't changed within this number of events
73
+ eventsBetweenSnapshots: 150, //how often (in events) to take a snapshot using the fingerprintFunction
74
+ debug: false //if true extra logging is made
75
+ },
76
+
77
+ // If the following line is uncommented, then screenshots are taken
78
+ // every "n" seconds.
79
+ screenshotInterval: 5,
80
+
81
+ // Events are triggered based on the relative weights here. The event
82
+ // with this highest number gets triggered the most.
83
+ //
84
+ // If you want to add your own "events", check out the event method
85
+ // definitions below.
86
+ eventWeights: {
87
+ tap: 500,
88
+ drag: 10,
89
+ flick: 1,
90
+ orientation: 1,
91
+ clickVolumeUp: 1,
92
+ clickVolumeDown: 1,
93
+ lock: 1,
94
+ pinchClose: 10,
95
+ pinchOpen: 10,
96
+ shake: 1
97
+ },
98
+
99
+ // Probability that touch events will have these different properties
100
+ touchProbability: {
101
+ multipleTaps: 0.05,
102
+ multipleTouches: 0.05,
103
+ longPress: 0.05
104
+ }
105
+
106
+
107
+ // Uncomment the following to restrict events to a rectangluar area of
108
+ // the screen
109
+ /*
110
+ frame: {
111
+ origin: {x: 0, y: 0},
112
+ size: {width: 100, height: 50}
113
+ }
114
+ */
115
+
116
+ };
117
+
118
+ // Dismiss alerts
119
+ UIATarget.onAlert = function onAlert(alert) {
120
+ var title = alert.name();
121
+ UIALogger.logMessage('On Alert: ' + title);
122
+ return true;
123
+ }
124
+
125
+ }
126
+
127
+ // --- --- --- ---
128
+ // Event Methods
129
+ //
130
+ // Any event probability in the hash above corresponds to a related event
131
+ // method below. So, a "tap" probability will trigger a "tap" method.
132
+ //
133
+ // If you want to add your own events, just add a probability to the hash
134
+ // above and then add a corresponding method below. Boom!
135
+ //
136
+ // Each event method can call any other method on this UIAutoMonkey object.
137
+ // All the methods at the end are helpers at your disposal and feel free to
138
+ // add your own.
139
+
140
+ UIAutoMonkey.prototype.allEvents = {
141
+ tap: function() {
142
+ this.target().tapWithOptions(
143
+ { x: this.randomX(), y: this.randomY() },
144
+ {
145
+ tapCount: this.randomTapCount(),
146
+ touchCount: this.randomTouchCount(),
147
+ duration: this.randomTapDuration()
148
+ }
149
+ );
150
+ },
151
+
152
+ drag: function() {
153
+ this.target().dragFromToForDuration(
154
+ { x: this.randomX(), y: this.randomY() },
155
+ { x: this.randomX(), y: this.randomY() },
156
+ 0.5
157
+ );
158
+ },
159
+
160
+ flick: function() {
161
+ this.target().flickFromTo(
162
+ { x: this.randomX(), y: this.randomY() },
163
+ { x: this.randomX(), y: this.randomY() }
164
+ );
165
+ },
166
+
167
+ orientation: function() {
168
+ var orientations = [
169
+ UIA_DEVICE_ORIENTATION_PORTRAIT,
170
+ UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN,
171
+ UIA_DEVICE_ORIENTATION_LANDSCAPELEFT,
172
+ UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT
173
+ ];
174
+
175
+ var i = Math.floor(Math.random() * 10) % orientations.length;
176
+ var newOrientation = orientations[i];
177
+ this.target().setDeviceOrientation(newOrientation);
178
+ this.delay(0.9);
179
+ },
180
+
181
+ clickVolumeUp: function() {
182
+ this.target().clickVolumeUp();
183
+ },
184
+
185
+ clickVolumeDown: function() {
186
+ this.target().clickVolumeUp();
187
+ },
188
+
189
+ lock: function() {
190
+ this.target().lockForDuration(Math.random() * 3);
191
+ },
192
+
193
+ pinchClose: function () {
194
+ this.target().pinchCloseFromToForDuration(
195
+ { x: this.randomX(), y: this.randomY() },
196
+ { x: this.randomX(), y: this.randomY() },
197
+ 0.5
198
+ );
199
+ },
200
+
201
+ pinchOpen: function () {
202
+ this.target().pinchOpenFromToForDuration(
203
+ { x: this.randomX(), y: this.randomY() },
204
+ { x: this.randomX(), y: this.randomY() },
205
+ 0.5
206
+ );
207
+ },
208
+
209
+ shake: function() {
210
+ this.target().shake();
211
+ }
212
+ };
213
+
214
+ // --- --- --- ---
215
+ // Helper methods
216
+ //
217
+ UIAutoMonkey.prototype.RELEASE_THE_MONKEY = function() {
218
+ // Called at the bottom of this script to, you know...
219
+ //
220
+ // RELEASE THE MONKEY!
221
+ try{
222
+ if (this.config.minutesToRun && this.config.numberOfEvents) {
223
+ throw "invalid configuration. You cannot define both minutesToRun and numberOfEvents"
224
+ }
225
+ var conditionHandlers = this.config.conditionHandlers || []; //For legacy configs, if not present default to empty.
226
+ var useConditionHandlers = conditionHandlers.length > 0;
227
+ var checkTime = false;
228
+ var localNumberOfEvents = this.config.numberOfEvents; //we may modify so we want to leave config untouched
229
+ if (this.config.minutesToRun) {
230
+ checkTime = true;
231
+ localNumberOfEvents = 2000000000;
232
+ var startTime = new Date().getTime();
233
+ var checkTimeEvery = this.config.checkTimeEvery || 60; //number of events to pass before we check the time
234
+ }
235
+ //setup anr parameters as needed
236
+ var anrFingerprintFunction = this.config.anrSettings ? this.config.anrSettings.fingerprintFunction : false; //handle legacy settings missing this
237
+ if (anrFingerprintFunction) {
238
+ this.anrSnapshot = "Initial snapshot-nothing should match this!!";
239
+ this.anrSnapshotTakenAtIndex = -1;
240
+ var anrEventsBetweenSnapshots = this.config.anrSettings.eventsBetweenSnapshots || 300;
241
+ var anrDebug = this.config.anrSettings.debug;
242
+ this.anrMaxElapsedCount = -1;
243
+ }
244
+
245
+ UIALogger.logMessage(JSON.stringify(UIATarget.localTarget().rect().size));
246
+
247
+ for (var i = 0; i < localNumberOfEvents; i++) {
248
+ if (checkTime && (i % checkTimeEvery == 0)) { //check the time if needed
249
+ var currTime = new Date().getTime();
250
+ var elapsedMinutes = (currTime-startTime) / 60000;
251
+ if (elapsedMinutes >= this.config.minutesToRun) {
252
+ UIALogger.logDebug("Ending monkey after " + elapsedMinutes + " minutes run time.");
253
+ break;
254
+ } else {
255
+ UIALogger.logDebug(this.config.minutesToRun - elapsedMinutes + " minutes left to run.")
256
+ }
257
+ }
258
+ this.triggerRandomEvent();
259
+ if (anrFingerprintFunction && (i % anrEventsBetweenSnapshots == 0)) this.anrCheck(i, anrFingerprintFunction, anrDebug);
260
+ // if (this.config.screenshotInterval) this.takeScreenShotIfItIsTime();
261
+ // this.takeScreenShotIfItIsTime();
262
+ if (useConditionHandlers) this.processConditionHandlers(conditionHandlers, i+1, this.target());
263
+ this.delay();
264
+ }
265
+ // publish stats if warranted
266
+ if (anrFingerprintFunction) {
267
+ UIALogger.logDebug("ANR Statistics");
268
+ UIALogger.logDebug("ANR max event count for identical fingerprint snapshots :: events before ANR declared: " + this.anrMaxElapsedCount + " :: " + this.config.anrSettings.eventsBeforeANRDeclared);
269
+ }
270
+
271
+ if (useConditionHandlers) {
272
+ UIALogger.logDebug("ConditionHandler Statistics")
273
+ conditionHandlers.forEach(function(aHandler) {aHandler.logStats()});
274
+ conditionHandlers.sort(function(aHandler, bHandler) {return aHandler.statsHandleInvokedCount - bHandler.statsHandleInvokedCount});
275
+ UIALogger.logDebug("sorted by HandleInvokedCount");
276
+ conditionHandlers.forEach(function(aHandler) {UIALogger.logDebug(aHandler + ": " + aHandler.statsHandleInvokedCount)});
277
+ }
278
+ }finally{
279
+ UIALogger.logDebug("MonkeyTest finish.");
280
+ }
281
+ };
282
+
283
+
284
+ UIAutoMonkey.prototype.anrCheck = function(i, fingerprintFunction, debugFlag){
285
+
286
+ var newSnapshot = fingerprintFunction();
287
+ if (newSnapshot != this.anrSnapshot) {
288
+ //all is good, we're moving along
289
+ if (debugFlag) UIALogger.logDebug("UIAutoMonkey:anrCheck(): snapshot != for event " + i);
290
+ this.anrSnapshot = newSnapshot;
291
+ this.anrSnapshotTakenAtIndex = i;
292
+ }
293
+ else {
294
+ //have a match
295
+ //for how many counts?
296
+ var elapsedCount = i - this.anrSnapshotTakenAtIndex;
297
+ this.anrMaxElapsedCount = Math.max(this.anrMaxElapsedCount, elapsedCount);
298
+ UIALogger.logDebug("UIAutoMonkey:anrCheck(): snapshot == with elapsed count=" + elapsedCount);
299
+ if (elapsedCount > this.config.anrSettings.eventsBeforeANRDeclared) {
300
+ UIALogger.logDebug("duplicate snapshot detected" + this.anrSnapshot);
301
+ throw "anr exception-identical after " + elapsedCount + " events";
302
+ };
303
+ };
304
+ };
305
+
306
+
307
+ UIAutoMonkey.prototype.processConditionHandlers = function(conditionHandlers, eventNumberPlus1, target) {
308
+ var mainWindow = target.frontMostApp().mainWindow(); //optimization to let handlers do less work. Assumes isTrue() doesn't alter the mainWindow.
309
+ for (var i = 0; i < conditionHandlers.length; i++) {
310
+ var aCondition = conditionHandlers[i];
311
+ if ((eventNumberPlus1 % aCondition.checkEvery()) != 0) {
312
+ continue; //not yet time to process aCondition.
313
+ }
314
+ try {
315
+ UIATarget.localTarget().pushTimeout(0);
316
+ var isConditionTrue = aCondition.isTrue(target, eventNumberPlus1, mainWindow);
317
+ }
318
+ finally {
319
+ UIATarget.localTarget().popTimeout();
320
+ }
321
+ if (isConditionTrue) {
322
+ aCondition.handle(target, mainWindow);
323
+ if (aCondition.isExclusive()) {
324
+ break;
325
+ } else {
326
+ mainWindow = target.frontMostApp().mainWindow(); //could be stale
327
+ }
328
+ };
329
+ };
330
+ };
331
+
332
+ UIAutoMonkey.prototype.triggerRandomEvent = function() {
333
+ var name = this.chooseEventName();
334
+ // Find the event method based on the name of the event
335
+ var event = this.allEvents[name];
336
+ event.apply(this);
337
+ this.takeScreenShotIfItIsTime();
338
+ };
339
+
340
+ UIAutoMonkey.prototype.target = function() {
341
+ // Return the local target.
342
+ return UIATarget.localTarget();
343
+ };
344
+
345
+ UIAutoMonkey.prototype.delay = function(seconds) {
346
+ // Delay the target by `seconds` (can be a fraction)
347
+ // Defaults to setting in configuration
348
+ seconds = seconds || this.config.delayBetweenEvents;
349
+ this.target().delay(seconds);
350
+ };
351
+
352
+ UIAutoMonkey.prototype.chooseEventName = function() {
353
+ // Randomly chooses an event name from the `eventsWeight` dictionary
354
+ // based on the given weights.
355
+ var calculatedEventWeights = [];
356
+ var totalWeight = 0;
357
+ var events = this.config.eventWeights;
358
+ for (var event in events) {
359
+ if (events.hasOwnProperty(event)) {
360
+ calculatedEventWeights.push({
361
+ weight: events[event]+totalWeight,
362
+ event: event
363
+ });
364
+ totalWeight += events[event];
365
+ }
366
+ }
367
+
368
+ var chosenWeight = Math.random() * 1000 % totalWeight;
369
+
370
+ for (var i = 0; i < calculatedEventWeights.length; i++) {
371
+ if (chosenWeight < calculatedEventWeights[i].weight) {
372
+ return calculatedEventWeights[i].event;
373
+ }
374
+ }
375
+
376
+ throw "No event was chosen!";
377
+ };
378
+
379
+ UIAutoMonkey.prototype.screenWidth = function() {
380
+ // Need to adjust by one to stay within rectangle
381
+ return this.target().rect().size.width - 1;
382
+ };
383
+
384
+ UIAutoMonkey.prototype.screenHeight = function() {
385
+ // Need to adjust by one to stay within rectangle
386
+ return this.target().rect().size.height - 1;
387
+ };
388
+
389
+ UIAutoMonkey.prototype.randomX = function() {
390
+ var min, max;
391
+ var r = Math.random();
392
+
393
+ if (this.config.frame){
394
+ // Limits coordinates to given frame if set in config
395
+ min = this.config.frame.origin.x;
396
+ max = this.config.frame.size.width + min;
397
+ } else {
398
+ // Returns a random X coordinate within the screen rectangle
399
+ min = 0;
400
+ max = this.screenWidth();
401
+ }
402
+ return r == 0 ? 1 : r * (max - min) + min;
403
+ };
404
+
405
+ UIAutoMonkey.prototype.randomY = function() {
406
+ var min, max;
407
+ var r = Math.random();
408
+
409
+ if (this.config.frame){
410
+ // Limits coordinates to given frame if set in config
411
+ min = this.config.frame.origin.y;
412
+ max = this.config.frame.size.height + min;
413
+ } else {
414
+ // Returns a random Y coordinate within the screen rectangle
415
+ min = 0;
416
+ max = this.screenHeight();
417
+ }
418
+ return r == 0 ? 1 : r * (max - min) + min;
419
+ };
420
+
421
+ UIAutoMonkey.prototype.randomTapCount = function() {
422
+ // Calculates a tap count for tap events based on touch probabilities
423
+ if (this.config.touchProbability.multipleTaps > Math.random()) {
424
+ return Math.floor(Math.random() * 10) % 3 + 1;
425
+ }
426
+ else return 1;
427
+ };
428
+
429
+ UIAutoMonkey.prototype.randomTouchCount = function() {
430
+ // Calculates a touch count for tap events based on touch probabilities
431
+ if (this.config.touchProbability.multipleTouches > Math.random()) {
432
+ return Math.floor(Math.random() * 10) % 3 + 1;
433
+ }
434
+ else return 1;
435
+ };
436
+
437
+ UIAutoMonkey.prototype.randomTapDuration = function() {
438
+ // Calculates whether or not a tap should be a long press based on
439
+ // touch probabilities
440
+ if (this.config.touchProbability.longPress > Math.random()) {
441
+ return 0.5;
442
+ }
443
+ else return 0;
444
+ };
445
+
446
+ UIAutoMonkey.prototype.randomRadians = function() {
447
+ // Returns a random radian value
448
+ return Math.random() * 10 % (3.14159 * 2);
449
+ };
450
+
451
+ UIAutoMonkey.prototype.takeScreenShotIfItIsTime = function() {
452
+ // var now = (new Date()).valueOf();
453
+ // if (!this._lastScreenshotTime) this._lastScreenshotTime = 0;
454
+
455
+ // if (now - this._lastScreenshotTime > this.config.screenshotInterval * 1000) {
456
+ // var filename = "monkey-" + (new Date()).toISOString().replace(/[:\.]+/g, "-");
457
+ // this.target().captureScreenWithName(filename);
458
+ // this._lastScreenshotTime = now;
459
+ // }
460
+ var filename = "monkey-" + (new Date()).toISOString().replace(/[:\.]+/g, "-");
461
+ this.target().captureScreenWithName(filename);
462
+ };
463
+
464
+ // Commodity function to call RELEASE_THE_MONKEY directly on UIAutoMonkey
465
+ // if you don't need to customize your instance
466
+ UIAutoMonkey.RELEASE_THE_MONKEY = function() {
467
+ (new UIAutoMonkey()).RELEASE_THE_MONKEY();
468
+ };
469
+
470
+ // UIAutoMonkey.RELEASE_THE_MONKEY();