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