monkey_master 1.0.0 → 1.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.
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+
6
+ notifications:
7
+ email:
8
+ recipients:
9
+ - lukas.nagl@innovaptor.com
10
+ on_failure: change
11
+ on_success: never
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
- *monkey_master* - A tool for conveniently employing Android adb monkeys.
1
+ `monkey_master` - A tool for conveniently employing Android adb monkeys.
2
2
  ================================================================
3
+
4
+ [![Build Status](https://travis-ci.org/j4zz/monkey_master.svg)](https://travis-ci.org/j4zz/monkey_master)
5
+
3
6
  Android's adb offers the ui/application exerciser [monkey](http://developer.android.com/tools/help/monkey.html). Conveniently employing it can be cumbersome, though:
4
7
 
5
8
  * It's inconvenient to kill a running monkey.
@@ -7,15 +10,15 @@ Android's adb offers the ui/application exerciser [monkey](http://developer.andr
7
10
  * You either watch the log in your (running) sdk, or you manually handle logcat.
8
11
  * Managing all of the above on multiple devices is a real pain.
9
12
 
10
- *monkey_master* is a convenience tool for solving these issues. It can easily be combined with other tools, for example to build a fully automated build & test system.
13
+ `monkey_master` is a convenience tool for solving these issues. It can easily be combined with other tools, for example to build a fully automated build & test system.
11
14
 
12
15
  Besides having convenience commands for starting and killing adb monkeys, it has multi-device support (simultaneously running monkeys on multiple devices) and automatically creates log files for each device.
13
16
 
14
- For an example of a *monkey_master* test setup, and the reasoning behind the project, visit: [http://innovaptor.com/blog/2013/08/18/building-an-automated-testing-and-error-reporting-system-for-android-apps-with-monkey-master-and-crashlytics.html](http://innovaptor.com/blog/2013/08/18/building-an-automated-testing-and-error-reporting-system-for-android-apps-with-monkey-master-and-crashlytics.html)
17
+ For an example of a `monkey_master` test setup, and the reasoning behind the project, visit [this blog post](http://innovaptor.com/blog/2013/08/18/building-an-automated-testing-and-error-reporting-system-for-android-apps-with-monkey-master-and-crashlytics.html).
15
18
 
16
19
  Installation
17
20
  ================================================================
18
- *monkey_master* is available as a ruby gem:
21
+ `monkey_master` is available as a ruby gem:
19
22
 
20
23
  gem install monkey_master
21
24
 
@@ -23,7 +26,7 @@ Installation
23
26
 
24
27
  export PATH=/YOUR/PATH/android-sdks/platform-tools:$PATH
25
28
 
26
- Furthermore, you need to have a device in development mode connected. Currently, *monkey_master* is not tested with an emulator. For a list of connected devices, use `adb devices`.
29
+ Furthermore, you need to have a device in *development mode* connected. Currently, `monkey_master` is not tested with an emulator. For a list of connected devices, use `adb devices`.
27
30
 
28
31
  Usage
29
32
  ================================================================
@@ -40,18 +43,34 @@ An example for a test run could be:
40
43
 
41
44
  monkey_master com.my.App --iterations 100
42
45
 
43
- If you want to stop the monkeys, either SIGINT the *monkey_master* during execution,
44
- or call:
46
+ If you want to stop the monkeys, either SIGINT (keyboard interrupt) the `monkey_master` during execution, or call:
45
47
 
46
48
  monkey_master -k
47
49
 
48
- If you have multiple devices connected, and want to use *monkey_master* on some of them only,
49
- call:
50
+ If you have multiple devices connected, and want to use `monkey_master` on some of them only, call:
50
51
 
51
52
  monkey_master com.my.App --devices DEVICEID1,DEVICEID2 --iterations 100
52
53
 
53
- Contributing
54
+ Crash Reporting
55
+ ================================================================
56
+ You might want to use `monkey_master` with a crash reporting tool such as [fabric](https://fabric.io/). Simply integrate the crash reporting library of your choice in your Android App, and `monkey_master`’s crashes will get reported there.
57
+
58
+ Android Edge Cases
54
59
  ================================================================
55
- The initial version of *monkey_master* has been created with little ruby knowledge and with the pressure of an immediate need.
60
+ Sometimes you encounter errors that only a monkey can generate, but that you can’t reasonably fix in your code. In case of such an error, you will want to exit gracefully if the user is a monkey (in order to suppress useless crash reports), but leave the error handling as it is for production.
56
61
 
62
+ Furthermore, you might want to disable certain network calls, or redirect them to a test server.
63
+
64
+ For such cases, there’s ActivityManager.isUserAMonkey():
65
+
66
+ ```java
67
+ if(ActivityManager.isUserAMonkey()) {
68
+ // Work on the test server
69
+ } else {
70
+ // Work on the production server
71
+ }
72
+ ```
73
+
74
+ Contributing
75
+ ================================================================
57
76
  Code style or beauty fixes are just as welcome as pull requests, bug reports or ideas.
data/Rakefile CHANGED
@@ -1,9 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
- require 'rake/testtask'
2
+ require 'rspec/core/rake_task'
3
3
 
4
- Rake::TestTask.new do |t|
5
- t.libs << 'test'
4
+ # Default directory to look in is `/specs`
5
+ # Run with `rake spec`
6
+ RSpec::Core::RakeTask.new(:spec) do |task|
7
+ task.rspec_opts = ['--color', '--format', 'documentation']
6
8
  end
7
9
 
8
- desc "Run tests"
9
- task :default => :test
10
+ task default: :spec
data/bin/monkey_master CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'docopt'
4
-
5
4
  require 'monkey_master'
6
5
 
7
6
  # Parse Command Line Options
@@ -9,44 +8,44 @@ doc = <<DOCOPT
9
8
  A tool for conveniently employing Android adb monkeys.
10
9
 
11
10
  Usage:
12
- #{__FILE__} <app_id> [--devices <devices>] [--iterations <iterations>] [-k]
11
+ #{__FILE__} <app_id> [--devices <devices>] [--iterations <iterations>] [-k] [--adb <adb_args>]
13
12
  #{__FILE__} -k
14
13
  #{__FILE__} -h | --help
15
14
  #{__FILE__} --version
16
15
 
17
16
  Options:
18
- -h --help Show this screen.
19
- --version Show version.
20
- --iterations <iterations> The number of monkeys that should be run consecutively.
21
- It is preferable to run a high number of iterations of short-lived monkeys in order to handle freezes better.
22
- --devices <devices> Devices which should be used by the monkey commander separated by a ','. If not given, uses all devices.
23
-
17
+ -h --help Show this screen.
18
+ --version Show version.
19
+ --iterations <iterations> The number of monkeys that should be run consecutively.
20
+ It is preferable to run a high number of iterations of short-lived monkeys in order to handle freezes better.
21
+ --devices <devices> Devices which should be used by the monkey commander separated by a ','. If not given, uses all devices.
22
+ --adb <adb_args> Arguments for running the adb monkey as a string, e.g. "--throttle 500". If not provided, reasonable defaults will be used.
24
23
  DOCOPT
25
24
 
26
25
  begin
27
- opts = Docopt::docopt(doc)
28
- commander = MonkeyMaster::MonkeyCommander.new(opts["<app_id>"])
29
-
30
- devices = opts["--devices"]
31
- commander.detect_devices(devices)
32
-
33
- if(opts["-k"])
34
- commander.kill_monkeys
35
- end
36
-
37
- if(opts["<app_id>"])
38
- # An app id has been given, proceed with starting monkeys on the devices
39
- iterations = opts["--iterations"]
40
- if(iterations)
41
- commander.iterations = iterations
42
- end
43
- commander.command_monkeys
44
- end
45
- exit 0
26
+ opts = Docopt::docopt(doc)
27
+ unless MonkeyMaster::ADB.adb?
28
+ puts 'ERROR: adb is not installed or not accessible'
29
+ exit 2
30
+ end
31
+ commander = MonkeyMaster::MonkeyCommander.new(opts['<app_id>'])
32
+
33
+ devices = opts['--devices']
34
+ commander.detect_devices(devices)
35
+
36
+ commander.kill_monkeys if opts['-k']
37
+
38
+ if opts['<app_id>']
39
+ # An app id has been given, proceed with starting monkeys on the devices
40
+ iterations = opts['--iterations']
41
+ commander.iterations = iterations if iterations
42
+ commander.command_monkeys(opts['--adb'])
43
+ end
44
+ exit 0
46
45
  rescue Docopt::Exit => e
47
- puts e.message
46
+ puts e.message
48
47
  rescue ArgumentError => e
49
- puts "ERROR: Invalid arguments: " + e.message
48
+ puts "ERROR: Invalid arguments: #{e.message}"
50
49
  end
51
50
 
52
- exit 1
51
+ exit 1
data/lib/monkey_master.rb CHANGED
@@ -2,4 +2,4 @@ module MonkeyMaster
2
2
  end
3
3
 
4
4
  require 'monkey_master/version'
5
- require 'monkey_master/monkey_commander.rb'
5
+ require 'monkey_master/monkey_commander'
@@ -0,0 +1,81 @@
1
+ require 'mkmf'
2
+ require 'pry'
3
+
4
+ module MonkeyMaster
5
+ # Provide helpers to work with Android ADB
6
+ class ADB
7
+ # Run the adb monkey.
8
+ #
9
+ # +app_id+:: ID of the android app for which the monkey should be run
10
+ # +device+:: Device on which the adb monkey should be run
11
+ # +args+:: Arguments passed to the adb monkey
12
+ def self.monkey_run(app_id, device, args)
13
+ `adb -s #{device} shell monkey -p #{app_id} #{args}`
14
+ $?.exitstatus
15
+ end
16
+
17
+ # Force stop a monkey for an app.
18
+ #
19
+ # +app_id+:: ID of the android app for which the monkey should be stopped
20
+ def self.monkey_stop(app_id, device)
21
+ `adb -s #{device} shell am force-stop #{app_id}`
22
+ end
23
+
24
+ # Use ADB to detect connected Android devices.
25
+ def self.detect_devices
26
+ device_list = `adb devices | grep -v "List" | grep "device" | awk '{print $1}'`
27
+ device_list.split("\n")
28
+ end
29
+
30
+ # Kill ADB monkeys.
31
+ #
32
+ # +device+:: Devices for which the adb monkey should be killed
33
+ def self.kill_monkeys(devices)
34
+ unless devices
35
+ puts '[ADB] No devices specified yet.'
36
+ return
37
+ end
38
+
39
+ devices.each do |device|
40
+ puts "[ADB] KILLING the monkey on device #{device}."
41
+ `adb -s #{device} shell ps | awk '/com\.android\.commands\.monkey/ { system("adb -s #{device} shell kill " $2) }'`
42
+ end
43
+ end
44
+
45
+ # Start logging on a certain device with logcat
46
+ #
47
+ # +app_id+:: App for which logs should be retrieved
48
+ # +device+:: Device for which logging should be started
49
+ # +log+:: File that should be used for logging
50
+ def self.start_logging(app_id, device, log)
51
+ begin
52
+ timeout(5) do
53
+ puts "[ADB/LOGS] Logging device #{device} to #{log}."
54
+ `adb -s #{device} logcat -c #{log} &`
55
+ `adb -s #{device} logcat #{app_id}:W > #{log} &`
56
+ end
57
+ rescue Timeout::Error
58
+ end_logging
59
+ raise ArgumentError, 'It doesn’t seem like there are ready, connected devices.'
60
+ end
61
+ end
62
+
63
+ # End logging on multiple devices.
64
+ #
65
+ # +device+:: Devices for which logging should be stopped
66
+ def self.end_logging(devices)
67
+ devices.each do |device|
68
+ puts "[ADB/LOGS] KILLING the logcat process on device #{device}."
69
+ `adb -s #{device} shell ps | grep -m1 logcat | awk '{print $2}' | xargs adb -s #{device} shell kill`
70
+ puts "[ADB/LOGS] KILLING the logcat process for the device #{device} on the machine."
71
+ `ps ax | grep -m1 "adb -s #{device} logcat" | awk '{print $1}' | xargs kill`
72
+ end
73
+ end
74
+
75
+ # Check if adb is accessible as an executable
76
+ def self.adb?
77
+ adb = find_executable 'adb'
78
+ adb.nil? ? false : true
79
+ end
80
+ end
81
+ end
@@ -1,155 +1,131 @@
1
1
  require 'fileutils'
2
2
  require 'logger'
3
3
  require 'timeout'
4
+ require 'pry'
5
+ require_relative 'adb'
4
6
 
5
7
  module MonkeyMaster
6
- # A class for conveniently employing Android adb monkeys.
7
- #
8
- # Author:: Lukas Nagl (mailto:lukas.nagl@innovaptor.com)
9
- # Copyright:: Copyright (c) 2013 Innovaptor OG
10
- # License:: MIT
11
- class MonkeyCommander
12
- # Directory of the monkey logs.
13
- attr_reader :log_dir
14
- # Logger used for the monkey output.
15
- attr_reader :logger
16
- # The id of the app that should be tested by monkeys. E.g.: com.innovaptor.MonkeyTestApp
17
- attr_writer :app_id
18
- # The number of monkey iterations that should be run on each device.
19
- attr_writer :iterations
20
- # List of devices that should be used by the MonkeyCommander.
21
- attr_writer :device_list
22
- public
23
- # Initialize the monkey master.
24
- #
25
- # +app_id+:: The id of the app that should be tested by the monkeys, e.g. com.innovaptor.MonkeyTestApp
26
- def initialize(app_id)
27
- @app_id = app_id
28
- @iterations = 1 # Default to a single iteration
29
- @base_dir = Dir.pwd
30
- time = Time.new
31
- @log_dir = "monkey_logs" + time.strftime("%Y%m%d_%H%M%S")
32
- @logger = Logger.new(STDOUT)
33
- @logger.formatter = proc { |severity, datetime, progname, msg|
34
- "#{severity}|#{datetime}: #{msg}\n"
35
- }
36
- end
37
-
38
- # Either create a list of devices from the parameter,
39
- # or detect connected devices using adb.
40
- #
41
- # +devices+:: nil, for automatic device detection; or a list of device IDs separated by ','
42
- def detect_devices(devices)
43
- if(devices)
44
- # Devices are given, create a list
45
- devices = devices.split(',')
46
- @device_list = devices
47
- else
48
- # No devices specified, detect them
49
- device_list = %x(adb devices | grep -v "List" | grep "device" | awk '{print $1}')
50
- device_list = device_list.split("\n")
51
- @device_list = device_list
52
- end
53
- end
54
-
55
- # Kill the monkey on each device.
56
- def kill_monkeys
57
- if(@device_list)
58
- @device_list.each{|device|
59
- @logger.info("[CLEANUP] KILLING the monkey on device #{device}.")
60
- %x(adb -s #{device} shell ps | awk '/com\.android\.commands\.monkey/ { system("adb -s #{device} shell kill " $2) }')
61
- }
62
- else
63
- @logger.warn("[CLEANUP] No devices specified yet.")
64
- end
65
- end
66
-
67
- # Start running monkeys on all specified devices.
68
- def command_monkeys
69
- if(!@device_list || @device_list.empty?)
70
- raise ArgumentError, "No devices found or specified."
71
- end
72
- if(!@app_id)
73
- raise ArgumentError, "No app id specified."
74
- end
75
- prepare
76
-
77
- masters = []
78
- begin
79
- @device_list.each{|device|
80
- master = Thread.new{
81
- # Monkey around in parallel
82
-
83
- log_device_name = "monkey_current" + device + ".txt";
84
- current_log = File.join(@log_dir, log_device_name)
85
- start_logging(device, current_log)
86
- @logger.info("[MASTER #{device}] Starting to command monkeys.")
87
- @iterations.to_i.times do |i|
88
- @logger.info("\t[MASTER #{device}] Monkey " + i.to_s + " is doing its thing...")
89
-
90
- # Start the monkey
91
- %x(adb -s #{device} shell monkey -p #{@app_id} -v 80000 --throttle 100 --ignore-timeouts --pct-majornav 10 --pct-appswitch 0 --kill-process-after-error)
92
- if($? != 0)
93
- @logger.info("\t\t[MASTER #{device}] Monkey encountered an error!")
94
- end
95
-
96
- # Archive the log
97
- log_archiving_name = "monkeylog_" + device + "_" + i.to_s + ".txt"
98
- FileUtils.cp(current_log, File.join(@log_dir, log_archiving_name))
99
-
100
- # Clean the current log
101
- File.truncate(current_log, 0)
102
- @logger.info("\t\t[MASTER #{device}] Monkey " + i.to_s + " is killing the app now in preparation for the next monkey.")
103
- %x(adb -s #{device} shell am force-stop #{@app_id})
104
- end
105
- @logger.info("[MASTER #{device}] All monkeys are done.")
106
- }
107
- masters.push(master)
108
- }
109
-
110
- masters.each{|master| master.join} # wait for all masters to finish
111
- rescue SystemExit, Interrupt
112
- # Clean and graceful shutdown, if possible
113
- @logger.info("[MASTER] Received interrupt. Stopping all masters.")
114
- masters.each{|master| master.terminate}
115
- end
116
-
117
- kill_monkeys
118
- end_logging
119
- end
120
-
121
- private
122
- # Do all necessary preparations that are necessary for the monkeys to run.
123
- def prepare
124
- if(!File.directory?(@log_dir))
125
- Dir.mkdir(@log_dir);
126
- @logger.info("[SETUP] Writing to the following folder: #{@log_dir}")
127
- end
128
- kill_monkeys
129
- end
130
-
131
- # Start logging on all devices.
132
- def start_logging(device, current_log)
133
- begin
134
- Timeout::timeout(5) {
135
- @logger.info("[SETUP] Creating the following log file: #{current_log}")
136
- %x(adb -s #{device} logcat -c #{current_log} &)
137
- %x(adb -s #{device} logcat *:W > #{current_log} &)
138
- }
139
- rescue Timeout::Error
140
- end_logging
141
- raise ArgumentError, "It doesn't seem like there are ready, connected devices."
142
- end
143
- end
144
-
145
- # End logging on all devices.
146
- def end_logging
147
- @device_list.each{|device|
148
- @logger.info("[CLEANUP] KILLING the logcat process on device #{device}.")
149
- %x(adb -s #{device} shell ps | grep -m1 logcat | awk '{print $2}' | xargs adb -s #{device} shell kill)
150
- @logger.info("[CLEANUP] KILLING the logcat process for the device #{device} on the machine.")
151
- %x(ps ax | grep -m1 "adb -s #{device} logcat" | awk '{print $1}' | xargs kill)
152
- }
153
- end
154
- end
8
+ # A class for conveniently employing Android adb monkeys.
9
+ #
10
+ # Author:: Lukas Nagl (mailto:lukas.nagl@innovaptor.com)
11
+ # Copyright:: Copyright (c) 2013 Innovaptor OG
12
+ # License:: MIT
13
+ class MonkeyCommander
14
+ # Directory of the monkey logs.
15
+ attr_reader :log_dir
16
+ # Logger used for the monkey output.
17
+ attr_reader :logger
18
+ # The id of the app that should be tested by monkeys. E.g.: com.innovaptor.MonkeyTestApp
19
+ attr_writer :app_id
20
+ # The number of monkey iterations that should be run on each device.
21
+ attr_writer :iterations
22
+ # List of devices that should be used by the MonkeyCommander.
23
+ attr_writer :device_list
24
+
25
+ public
26
+
27
+ # Initialize the monkey master.
28
+ #
29
+ # +app_id+:: The id of the app that should be tested by the monkeys, e.g. com.innovaptor.MonkeyTestApp
30
+ def initialize(app_id)
31
+ @app_id = app_id
32
+ @iterations = 1 # Default to a single iteration
33
+ @base_dir = Dir.pwd
34
+ time = Time.new
35
+ @log_dir = 'monkey_logs' + time.strftime('%Y%m%d_%H%M%S')
36
+ @logger = Logger.new(STDOUT)
37
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
38
+ "#{severity}|#{datetime}: #{msg}\n"
39
+ end
40
+ end
41
+
42
+ # Either create a list of devices from the parameter,
43
+ # or detect connected devices using adb.
44
+ #
45
+ # +devices+:: nil, for automatic device detection; or a list of device IDs separated by ','
46
+ def detect_devices(devices)
47
+ @device_list = devices ? devices.split(',') : ADB.detect_devices
48
+ end
49
+
50
+ # Kill the monkey on all detected devices.
51
+ def kill_monkeys
52
+ ADB.kill_monkeys(@device_list)
53
+ end
54
+
55
+ # Start running monkeys on all specified devices.
56
+ #
57
+ # +adb_args+:: Arguments passed to the adb monkeys
58
+ def command_monkeys(adb_args='-v 80000 --throttle 200 --ignore-timeouts --pct-majornav 20 --pct-appswitch 0 --kill-process-after-error')
59
+ @logger.info("[SETUP] Will run adb monkeys with the following arguments: #{adb_args}")
60
+ if !@device_list || @device_list.empty?
61
+ fail(ArgumentError, 'No devices found or specified. Check if development mode is on.')
62
+ end
63
+
64
+ fail(ArgumentError, 'No app id specified.') unless @app_id
65
+
66
+ prepare
67
+
68
+ masters = []
69
+ begin
70
+ @device_list.each do |device|
71
+ master = Thread.new do
72
+ # Monkey around in parallel
73
+
74
+ device_log = log_for_device(@app_id, device)
75
+
76
+ @logger.info("[MASTER #{device}] Starting to command monkeys.")
77
+ @iterations.to_i.times do |i|
78
+ @logger.info("\t[MASTER #{device}] Monkey #{i} is doing its thing…")
79
+
80
+ # Start the monkey
81
+ if ADB.monkey_run(@app_id, device, adb_args) != 0
82
+ @logger.info("\t\t[MASTER #{device}] Monkey encountered an error!")
83
+ end
84
+
85
+ # Archive and clean the log
86
+ archive_and_clean_log(device_log, "monkeylog_#{device}_#{i}.txt")
87
+
88
+ @logger.info("\t\t[MASTER #{device}] Monkey #{i} is killing the app now in preparation for the next monkey.")
89
+ ADB.monkey_stop(@app_id, device)
90
+ end
91
+ @logger.info("[MASTER #{device}] All monkeys are done.")
92
+ end
93
+ masters.push(master)
94
+ end
95
+
96
+ masters.each(&:join) # wait for all masters to finish
97
+ rescue SystemExit, Interrupt
98
+ # Clean and graceful shutdown, if possible
99
+ @logger.info('[MASTER] Received interrupt. Stopping all masters.')
100
+ masters.each(&:terminate)
101
+ end
102
+
103
+ ADB.kill_monkeys(@device_list)
104
+ ADB.end_logging(@device_list)
105
+ end
106
+
107
+ private
108
+
109
+ def archive_and_clean_log(device_log, name)
110
+ FileUtils.cp(device_log, File.join(@log_dir, name))
111
+ File.truncate(device_log, 0)
112
+ end
113
+
114
+ # start monkey log for a certain device
115
+ def log_for_device(app_id, device)
116
+ log_device_name = "monkey_current#{device}.txt"
117
+ log = File.join(@log_dir, log_device_name)
118
+ ADB.start_logging(app_id, device, log)
119
+ log
120
+ end
121
+
122
+ # Do all necessary preparations that are necessary for the monkeys to run.
123
+ def prepare
124
+ unless File.directory?(@log_dir)
125
+ Dir.mkdir(@log_dir)
126
+ @logger.info("[SETUP] Writing to the following folder: #{@log_dir}")
127
+ end
128
+ ADB.kill_monkeys(@device_list)
129
+ end
130
+ end
155
131
  end