smart_monkey 0.1

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