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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +57 -0
- data/Troubleshooting.md +61 -0
- data/VERSION +1 -0
- data/bin/smart_monkey +53 -0
- data/lib/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/lib/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/lib/bootstrap/css/bootstrap.css +6167 -0
- data/lib/bootstrap/css/bootstrap.min.css +9 -0
- data/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/lib/bootstrap/img/glyphicons-halflings.png +0 -0
- data/lib/bootstrap/js/bootstrap.js +2280 -0
- data/lib/bootstrap/js/bootstrap.min.js +6 -0
- data/lib/ios_device_log/deviceconsole +0 -0
- data/lib/smart_monkey.rb +2 -0
- data/lib/smart_monkey/command_helper.rb +71 -0
- data/lib/smart_monkey/monkey_runner.rb +549 -0
- data/lib/smart_monkey/templates/automation_result.xsl +61 -0
- data/lib/smart_monkey/templates/index.html.erb +77 -0
- data/lib/smart_monkey/templates/result.html.erb +110 -0
- data/lib/smart_monkey/templates/result_view.coffee +160 -0
- data/lib/smart_monkey/templates/result_view.js +250 -0
- data/lib/ui-auto-monkey/UIAutoMonkey.js +470 -0
- data/lib/ui-auto-monkey/custom.js +73 -0
- data/lib/ui-auto-monkey/handler/buttonHandler.js +111 -0
- data/lib/ui-auto-monkey/handler/wbScrollViewButtonHandler.js +114 -0
- data/lib/ui-auto-monkey/tuneup/LICENSE +20 -0
- data/lib/ui-auto-monkey/tuneup/assertions.js +402 -0
- data/lib/ui-auto-monkey/tuneup/image_asserter +26 -0
- data/lib/ui-auto-monkey/tuneup/image_assertion.js +65 -0
- data/lib/ui-auto-monkey/tuneup/image_assertion.rb +102 -0
- data/lib/ui-auto-monkey/tuneup/lang-ext.js +76 -0
- data/lib/ui-auto-monkey/tuneup/screen.js +11 -0
- data/lib/ui-auto-monkey/tuneup/test.js +71 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/abbreviated_console_output.rb +38 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/colored_console_output.rb +27 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/console_output.rb +17 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/preprocessor.rb +25 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/run +343 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/xunit_output.rb +114 -0
- data/lib/ui-auto-monkey/tuneup/tuneup.js +6 -0
- data/lib/ui-auto-monkey/tuneup/tuneup_js.podspec +52 -0
- data/lib/ui-auto-monkey/tuneup/uiautomation-ext.js +965 -0
- data/smart_monkey.gemspec +112 -0
- data/spec/spec_helper.rb +12 -0
- metadata +192 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "image_assertion"))
|
4
|
+
|
5
|
+
SUCCESS_EXIT_STATUS = 0
|
6
|
+
FAILED_EXIT_STATUS = 1
|
7
|
+
|
8
|
+
test_output = ARGV[0]
|
9
|
+
ref_images_path = ARGV[1]
|
10
|
+
image_name = ARGV[2]
|
11
|
+
threshold = ARGV[3]
|
12
|
+
|
13
|
+
unless (test_output && ref_images_path && image_name)
|
14
|
+
|
15
|
+
$stderr.puts('incorrect arguments')
|
16
|
+
exit FAILED_EXIT_STATUS
|
17
|
+
end
|
18
|
+
|
19
|
+
assertion_result = ImageAssertion.assert_image(File.expand_path(test_output),
|
20
|
+
File.expand_path(ref_images_path),
|
21
|
+
image_name,
|
22
|
+
threshold)
|
23
|
+
exit FAILED_EXIT_STATUS unless assertion_result
|
24
|
+
|
25
|
+
exit SUCCESS_EXIT_STATUS
|
26
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
var ImageAsserter = (function() {
|
2
|
+
|
3
|
+
function ImageAsserter(tuneUpPath, outputPath, refImagesPath) {
|
4
|
+
if (!outputPath || !refImagesPath || !tuneUpPath) {
|
5
|
+
throw new AssertionException("output, refImages, tuneUp pathes can't be null");
|
6
|
+
}
|
7
|
+
|
8
|
+
this.tuneUpPath = tuneUpPath;
|
9
|
+
this.outputPath = outputPath;
|
10
|
+
this.refImagesPath = refImagesPath;
|
11
|
+
|
12
|
+
var target = UIATarget.localTarget();
|
13
|
+
if (!target) {
|
14
|
+
throw new AssertionException("unable to get localTarget");
|
15
|
+
}
|
16
|
+
|
17
|
+
this.host = target.host();
|
18
|
+
if (!this.host) {
|
19
|
+
throw new AssertionException("unable to get current UAIHost");
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
ImageAsserter.prototype.assertImageNamed = function(imageName, threshold) {
|
24
|
+
var command,
|
25
|
+
taskResult,
|
26
|
+
assertSuccessfull = false,
|
27
|
+
SUCCESS_EXIT_CODE = 0,
|
28
|
+
TIMEOUT = 5,
|
29
|
+
args = [this.outputPath, this.refImagesPath, imageName, threshold];
|
30
|
+
|
31
|
+
command = this.tuneUpPath + '/image_asserter';
|
32
|
+
taskResult = this.host.performTaskWithPathArgumentsTimeout(command,
|
33
|
+
args,
|
34
|
+
TIMEOUT);
|
35
|
+
|
36
|
+
assertSuccessful = (taskResult.exitCode === SUCCESS_EXIT_CODE);
|
37
|
+
if (!assertSuccessful) {
|
38
|
+
UIALogger.logError(taskResult.stderr);
|
39
|
+
}
|
40
|
+
|
41
|
+
return assertSuccessful;
|
42
|
+
};
|
43
|
+
|
44
|
+
return ImageAsserter;
|
45
|
+
}());
|
46
|
+
|
47
|
+
function createImageAsserter(tuneUpPath, outputPath, refImagesPath) {
|
48
|
+
this.imageAsserter = new ImageAsserter(tuneUpPath, outputPath, refImagesPath);
|
49
|
+
}
|
50
|
+
|
51
|
+
function assertScreenMatchesImageNamed(imageName, message, threshold) {
|
52
|
+
if (!this.imageAsserter) {
|
53
|
+
throw new AssertionException("imageAsserter isn't created.");
|
54
|
+
}
|
55
|
+
|
56
|
+
UIATarget.localTarget().captureAppScreenWithName(imageName);
|
57
|
+
UIATarget.localTarget().delay(1); // delay for screenshot to be saved
|
58
|
+
|
59
|
+
var assertionPassed = this.imageAsserter.assertImageNamed(imageName, threshold);
|
60
|
+
if (!assertionPassed) {
|
61
|
+
|
62
|
+
if (!message) message = 'Assertion of the image ' + imageName + ' failed.';
|
63
|
+
throw new AssertionException(message);
|
64
|
+
}
|
65
|
+
}
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
class ImageAssertion
|
5
|
+
|
6
|
+
MAX_ALLOWED_DIFF_VALUE = 1.0
|
7
|
+
DIFF_IMAGE_FOLDER_NAME = 'screens_diff'
|
8
|
+
|
9
|
+
def self.assert_image(test_output, ref_images_path, image_name, threshold)
|
10
|
+
return false unless (test_output && ref_images_path && image_name)
|
11
|
+
|
12
|
+
diff_images_path = File.join(test_output, DIFF_IMAGE_FOLDER_NAME)
|
13
|
+
Dir.mkdir(diff_images_path) unless File.directory?(diff_images_path)
|
14
|
+
|
15
|
+
image_file_name = image_name + '.png'
|
16
|
+
expected_path = File.join(ref_images_path, image_file_name)
|
17
|
+
diff_path = File.join(diff_images_path, image_file_name)
|
18
|
+
|
19
|
+
run_folder_name = find_last_folder(test_output)
|
20
|
+
received_path = File.join(run_folder_name, image_file_name)
|
21
|
+
|
22
|
+
print_status(create_status('started', "Asserting #{image_file_name}."))
|
23
|
+
|
24
|
+
if !File.exists?(received_path)
|
25
|
+
error = "No captured image file found at #{received_path}"
|
26
|
+
print_status(create_status('failed', error))
|
27
|
+
return false
|
28
|
+
elsif !File.exists?(expected_path)
|
29
|
+
error = "No reference image file found at #{expected_path}"
|
30
|
+
print_status(create_status('failed', error))
|
31
|
+
return false
|
32
|
+
else
|
33
|
+
result, exit_status = im_compare(expected_path, received_path, diff_path)
|
34
|
+
if exit_status == 0
|
35
|
+
return process_imagemagick_result(image_file_name, result, threshold)
|
36
|
+
elsif exit_status == 1
|
37
|
+
print_status(create_status('failed', "Images differ, check #{diff_images_path} for details. ImageMagick error: #{result}"))
|
38
|
+
else
|
39
|
+
print_status(create_status('failed', "ImageMagick comparison failed: #{result}"))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Iterte through folders with name Run* and return with latests run number
|
47
|
+
def self.find_last_folder(test_output)
|
48
|
+
folder_mask = "#{test_output}/Run";
|
49
|
+
run_folders = Dir.glob("#{folder_mask}*")
|
50
|
+
|
51
|
+
return nil unless run_folders.length > 0
|
52
|
+
|
53
|
+
run_folders.sort do |x, y|
|
54
|
+
y.gsub(folder_mask, '').to_i <=> x.gsub(folder_mask, '').to_i
|
55
|
+
end[0]
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.process_imagemagick_result(image_file_name, result, threshold)
|
59
|
+
result_status = 'failed'
|
60
|
+
result_message = "#{image_file_name} is not equal to the reference."
|
61
|
+
assertionResult = false
|
62
|
+
|
63
|
+
#imagemagick outputs floating point metrics value when succeeds
|
64
|
+
compare_succeed = ( result.match(/[0-9]*\.?[0-9]+/).length > 0 )
|
65
|
+
threshold ||= MAX_ALLOWED_DIFF_VALUE
|
66
|
+
|
67
|
+
if compare_succeed
|
68
|
+
if result.to_f < threshold
|
69
|
+
|
70
|
+
result_status = 'passed'
|
71
|
+
result_message = "#{image_file_name} asserted successfully."
|
72
|
+
assertionResult = true
|
73
|
+
else
|
74
|
+
print_status(create_status(result_status, "expected diff is smaller than #{threshold} but #{result.to_f}."))
|
75
|
+
end
|
76
|
+
else
|
77
|
+
|
78
|
+
result_message = result
|
79
|
+
end
|
80
|
+
|
81
|
+
print_status(create_status(result_status, result_message))
|
82
|
+
assertionResult
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.create_status(status, message)
|
86
|
+
"#{Time.new} #{status}: #{message}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.print_status(message)
|
90
|
+
$stderr.puts(message)
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.im_compare(expected_path, received_path, diff_path)
|
94
|
+
command = '/usr/local/bin/compare -metric MAE '
|
95
|
+
command << Shellwords.escape(expected_path) + ' '
|
96
|
+
command << Shellwords.escape(received_path) + ' '
|
97
|
+
command << ( diff_path ? Shellwords.escape(diff_path) : 'null:' )
|
98
|
+
|
99
|
+
_, _, stderr, wait_thr = Open3.popen3(command)
|
100
|
+
[stderr.read, wait_thr.value.exitstatus]
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
/**
|
2
|
+
* Extend +destination+ with +source+
|
3
|
+
*/
|
4
|
+
function extend(destination, source) {
|
5
|
+
for (var property in source) {
|
6
|
+
destination[property] = source[property];
|
7
|
+
}
|
8
|
+
return destination;
|
9
|
+
}
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Dump the properties out a String returned by the function.
|
13
|
+
*/
|
14
|
+
function dumpProperties(_this) {
|
15
|
+
var dumpStr = "";
|
16
|
+
for (var propName in _this) {
|
17
|
+
if (_this.hasOwnProperty(propName)) {
|
18
|
+
if (dumpStr !== "") {
|
19
|
+
dumpStr += ", ";
|
20
|
+
}
|
21
|
+
dumpStr += (propName + "=" + _this[propName]);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
return dumpStr;
|
26
|
+
};
|
27
|
+
|
28
|
+
function getMethods(_this) {
|
29
|
+
var methods = [];
|
30
|
+
for (var m in _this) {
|
31
|
+
//if (typeof _this[m] == "function" && _this.hasOwnProperty(m)) {
|
32
|
+
if (typeof _this[m] == "function") {
|
33
|
+
methods.push(m);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
return methods;
|
37
|
+
}
|
38
|
+
|
39
|
+
extend(Array.prototype, {
|
40
|
+
/**
|
41
|
+
* Applies the given function to each element in the array until a
|
42
|
+
* match is made. Otherwise returns null.
|
43
|
+
* */
|
44
|
+
contains: function(f) {
|
45
|
+
for (i = 0; i < this.length; i++) {
|
46
|
+
if (f(this[i])) {
|
47
|
+
return this[i];
|
48
|
+
}
|
49
|
+
}
|
50
|
+
return null;
|
51
|
+
},
|
52
|
+
|
53
|
+
unique: function() {
|
54
|
+
function onlyUnique(value, index, self) {
|
55
|
+
return self.indexOf(value) === index;
|
56
|
+
}
|
57
|
+
|
58
|
+
return this.filter(onlyUnique);
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
String.prototype.trim = function() {
|
63
|
+
return this.replace(/^\s+|\s+$/g,"");
|
64
|
+
};
|
65
|
+
|
66
|
+
String.prototype.ltrim = function() {
|
67
|
+
return this.replace(/^\s+/,"");
|
68
|
+
};
|
69
|
+
|
70
|
+
String.prototype.rtrim = function() {
|
71
|
+
return this.replace(/\s+$/,"");
|
72
|
+
};
|
73
|
+
|
74
|
+
String.prototype.lcfirst = function() {
|
75
|
+
return this.charAt(0).toLowerCase() + this.substr(1);
|
76
|
+
};
|
@@ -0,0 +1,71 @@
|
|
1
|
+
/**
|
2
|
+
* Run a new test with the given +title+ and function body, which will
|
3
|
+
* be executed within the proper test declarations for the UIAutomation
|
4
|
+
* framework. The given function will be handed a +UIATarget+ and
|
5
|
+
* a +UIApplication+ object which it can use to exercise and validate your
|
6
|
+
* application.
|
7
|
+
*
|
8
|
+
* The +options+ parameter is an optional object/hash thingie that
|
9
|
+
* supports the following:
|
10
|
+
* logTree -- a boolean to log the element tree when the test fails (default 'true')
|
11
|
+
*
|
12
|
+
* Example:
|
13
|
+
* test("Sign-In", function(target, application) {
|
14
|
+
* // exercise and validate your application.
|
15
|
+
* });
|
16
|
+
*
|
17
|
+
* The +title+ is checked against every element of a global TUNEUP_ONLY_RUN
|
18
|
+
* array. To check, each element is converted to a RegExp. The test is only
|
19
|
+
* executed, if one check succeeds. If TUNEUP_ONLY_RUN is not defined,
|
20
|
+
* no checks are performed.
|
21
|
+
*/
|
22
|
+
function test(title, f, options) {
|
23
|
+
if (typeof TUNEUP_ONLY_RUN !== 'undefined') {
|
24
|
+
for (var i = 0; i < TUNEUP_ONLY_RUN.length; i++) {
|
25
|
+
if (new RegExp("^" + TUNEUP_ONLY_RUN[i] + "$").test(title)) {
|
26
|
+
break;
|
27
|
+
}
|
28
|
+
if (i == TUNEUP_ONLY_RUN.length -1) {
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
if (!options) {
|
35
|
+
options = testCreateDefaultOptions();
|
36
|
+
}
|
37
|
+
target = UIATarget.localTarget();
|
38
|
+
application = target.frontMostApp();
|
39
|
+
UIALogger.logStart(title);
|
40
|
+
try {
|
41
|
+
target.logDeviceInfo();//打印设备信息
|
42
|
+
f(target, application);
|
43
|
+
UIALogger.logPass(title);
|
44
|
+
}
|
45
|
+
catch (e) {
|
46
|
+
UIALogger.logError(e.toString());
|
47
|
+
if (options.logStackTrace) UIALogger.logError(e.stack);
|
48
|
+
if (options.logTree) target.logElementTree();
|
49
|
+
if (options.logTreeJSON) application.mainWindow().logElementTreeJSON();
|
50
|
+
if (options.screenCapture) target.captureScreenWithName(title + '-fail');
|
51
|
+
UIALogger.logFail(title);
|
52
|
+
}
|
53
|
+
finally{
|
54
|
+
target.captureScreenWithName('finish');
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Helper function to isolate clients from additional option changes. Clients can use this function to get a new option object and then only change the options they care about, confident that any new options added since their
|
60
|
+
* code was created will contain the new default values.
|
61
|
+
* @returns {Object} containing the error options
|
62
|
+
*/
|
63
|
+
function testCreateDefaultOptions() {
|
64
|
+
return {
|
65
|
+
logStackTrace: false,
|
66
|
+
logTree: false,
|
67
|
+
logTreeJSON: false,
|
68
|
+
screenCapture: false
|
69
|
+
};
|
70
|
+
}
|
71
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class AbbreviatedConsoleOutput < ColoredConsoleOutput
|
2
|
+
|
3
|
+
def add_status(status, date, time, time_zone, msg)
|
4
|
+
message = format(status, msg)
|
5
|
+
if !message.nil?
|
6
|
+
puts message
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(status, msg)
|
11
|
+
output = nil
|
12
|
+
if status
|
13
|
+
output = self.message_for_status(status, msg);
|
14
|
+
output = colorize(output, STATUS_COLORS[status]) if STATUS_COLORS[status]
|
15
|
+
end
|
16
|
+
output
|
17
|
+
end
|
18
|
+
|
19
|
+
def message_for_status(status, msg)
|
20
|
+
message = nil
|
21
|
+
case status
|
22
|
+
when /^default/
|
23
|
+
message = " > #{msg}"
|
24
|
+
when /^start/
|
25
|
+
message = "\n> #{status.to_s.capitalize}: #{msg}"
|
26
|
+
when /^fail/
|
27
|
+
message = "X #{status.to_s.capitalize}: #{msg}"
|
28
|
+
when /^pass/
|
29
|
+
message = "#{status.to_s.capitalize}: #{msg}"
|
30
|
+
when /^warning/
|
31
|
+
message = " ! #{status.to_s.capitalize}: #{msg}"
|
32
|
+
when /^issue/
|
33
|
+
message = " ! #{status.to_s.capitalize}: #{msg}"
|
34
|
+
end
|
35
|
+
|
36
|
+
message
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ColoredConsoleOutput < ConsoleOutput
|
2
|
+
COLORS = {
|
3
|
+
:red => 31,
|
4
|
+
:green => 32,
|
5
|
+
:yellow => 33,
|
6
|
+
:cyan => 36
|
7
|
+
}
|
8
|
+
|
9
|
+
STATUS_COLORS = {
|
10
|
+
:start => :cyan,
|
11
|
+
:pass => :green,
|
12
|
+
:fail => :red,
|
13
|
+
:error => :red,
|
14
|
+
:warning => :yellow,
|
15
|
+
:issue => :yellow
|
16
|
+
}
|
17
|
+
|
18
|
+
def format(status, date, time, time_zone, msg)
|
19
|
+
output = super
|
20
|
+
output = colorize(output, STATUS_COLORS[status]) if STATUS_COLORS[status]
|
21
|
+
output
|
22
|
+
end
|
23
|
+
|
24
|
+
def colorize(text, color)
|
25
|
+
"\e[#{COLORS[color]}m#{text}\e[0m"
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class ConsoleOutput
|
2
|
+
def add(line)
|
3
|
+
puts line
|
4
|
+
end
|
5
|
+
|
6
|
+
def add_status(status, date, time, time_zone, msg)
|
7
|
+
puts "\n" if status == :start # add a blank line before each test to visually group the output
|
8
|
+
puts format(status, date, time, time_zone, msg)
|
9
|
+
end
|
10
|
+
|
11
|
+
def format(status, date, time, time_zone, msg)
|
12
|
+
"#{date} #{time} #{time_zone} #{status.to_s.capitalize}: #{msg}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
end
|
17
|
+
end
|