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.
- checksums.yaml +7 -7
- data/.gitignore +4 -1
- data/.rubocop.yml +865 -0
- data/.travis.yml +11 -0
- data/README.md +30 -11
- data/Rakefile +6 -5
- data/bin/monkey_master +29 -30
- data/lib/monkey_master.rb +1 -1
- data/lib/monkey_master/adb.rb +81 -0
- data/lib/monkey_master/monkey_commander.rb +125 -149
- data/lib/monkey_master/version.rb +1 -1
- data/monkey_master.gemspec +6 -3
- data/spec/monkey_master_spec.rb +106 -0
- data/spec/spec_helper.rb +7 -0
- metadata +97 -44
- data/test/test_monkey_master.rb +0 -50
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
1
|
+
`monkey_master` - A tool for conveniently employing Android adb monkeys.
|
2
2
|
================================================================
|
3
|
+
|
4
|
+
[](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
|
-
|
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
|
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
|
-
|
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,
|
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
|
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
|
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
|
-
|
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
|
-
|
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 '
|
2
|
+
require 'rspec/core/rake_task'
|
3
3
|
|
4
|
-
|
5
|
-
|
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
|
-
|
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
|
19
|
-
--version
|
20
|
-
--iterations <iterations>
|
21
|
-
|
22
|
-
--devices <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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
46
|
+
puts e.message
|
48
47
|
rescue ArgumentError => e
|
49
|
-
|
48
|
+
puts "ERROR: Invalid arguments: #{e.message}"
|
50
49
|
end
|
51
50
|
|
52
|
-
exit 1
|
51
|
+
exit 1
|
data/lib/monkey_master.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|