kraken-mobile 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +8 -0
- data/README.md +169 -0
- data/bin/kraken-mobile +91 -0
- data/bin/kraken-mobile-calabash-android.rb +85 -0
- data/bin/kraken-mobile-generate.rb +19 -0
- data/bin/kraken-mobile-helpers.rb +48 -0
- data/bin/kraken-mobile-setup.rb +50 -0
- data/calabash-android-features-skeleton/my_first.feature +11 -0
- data/calabash-android-features-skeleton/step_definitions/kraken_steps.rb +1 -0
- data/calabash-android-features-skeleton/support/app_installation_hooks.rb +25 -0
- data/calabash-android-features-skeleton/support/app_life_cycle_hooks.rb +10 -0
- data/calabash-android-features-skeleton/support/env.rb +1 -0
- data/lib/kraken-mobile.rb +29 -0
- data/lib/kraken-mobile/constants.rb +25 -0
- data/lib/kraken-mobile/helpers/command_helper.rb +38 -0
- data/lib/kraken-mobile/helpers/devices_helper/adb_helper.rb +157 -0
- data/lib/kraken-mobile/helpers/devices_helper/manager.rb +43 -0
- data/lib/kraken-mobile/helpers/feature_analyzer.rb +28 -0
- data/lib/kraken-mobile/helpers/feature_grouper.rb +48 -0
- data/lib/kraken-mobile/helpers/reporter.rb +381 -0
- data/lib/kraken-mobile/models/device.rb +32 -0
- data/lib/kraken-mobile/protocols/file_protocol.rb +126 -0
- data/lib/kraken-mobile/runners/calabash/android/android_runner.rb +130 -0
- data/lib/kraken-mobile/runners/calabash/android/apk_signer.rb +14 -0
- data/lib/kraken-mobile/runners/calabash/android/cucumber.rb +27 -0
- data/lib/kraken-mobile/runners/calabash/android/kraken_hooks.rb +15 -0
- data/lib/kraken-mobile/runners/calabash/android/kraken_steps.rb +3 -0
- data/lib/kraken-mobile/runners/calabash/android/monkey_helper.rb +139 -0
- data/lib/kraken-mobile/runners/calabash/android/operations.rb +44 -0
- data/lib/kraken-mobile/runners/calabash/android/steps/communication_steps.rb +57 -0
- data/lib/kraken-mobile/runners/calabash/monkey/monkey_runner.rb +113 -0
- data/lib/kraken-mobile/runners/runner.rb +18 -0
- data/lib/kraken-mobile/version.rb +5 -0
- data/reporter/assets/css/bootstrap.min.css +6 -0
- data/reporter/assets/css/dataTables.bootstrap.min.css +1 -0
- data/reporter/assets/css/feature_index.css +449 -0
- data/reporter/assets/css/font-awesome.min.css +4 -0
- data/reporter/assets/css/responsive.dataTables.min.css +1 -0
- data/reporter/assets/css/scenario_index.css +461 -0
- data/reporter/assets/fonts/FontAwesome.otf +0 -0
- data/reporter/assets/fonts/fontawesome-webfont.eot +0 -0
- data/reporter/assets/fonts/fontawesome-webfont.svg +2671 -0
- data/reporter/assets/fonts/fontawesome-webfont.ttf +0 -0
- data/reporter/assets/fonts/fontawesome-webfont.woff +0 -0
- data/reporter/assets/fonts/fontawesome-webfont.woff2 +0 -0
- data/reporter/assets/fonts/glyphicons-halflings-regular.eot +0 -0
- data/reporter/assets/fonts/glyphicons-halflings-regular.svg +288 -0
- data/reporter/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/reporter/assets/fonts/glyphicons-halflings-regular.woff +0 -0
- data/reporter/assets/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/reporter/assets/images/kraken.png +0 -0
- data/reporter/assets/js/Chart.min.js +14 -0
- data/reporter/assets/js/bootstrap.min.js +7 -0
- data/reporter/assets/js/d3.chart.min.js +7 -0
- data/reporter/assets/js/d3.v3.min.js +5 -0
- data/reporter/assets/js/dataTables.bootstrap.min.js +8 -0
- data/reporter/assets/js/dataTables.responsive.min.js +26 -0
- data/reporter/assets/js/first-sankey.js +292 -0
- data/reporter/assets/js/gitgraph.min.js +10 -0
- data/reporter/assets/js/html5shiv.min.js +4 -0
- data/reporter/assets/js/jquery-3.2.1.min.js +4 -0
- data/reporter/assets/js/jquery.dataTables.min.js +167 -0
- data/reporter/assets/js/respond.min.js +5 -0
- data/reporter/assets/js/sankey.js +512 -0
- data/reporter/feature_report.html.erb +274 -0
- data/reporter/index.html.erb +711 -0
- data/reporter/scenario_report.html.erb +174 -0
- metadata +169 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
Feature: Example feature
|
2
|
+
|
3
|
+
@user1
|
4
|
+
Scenario: As a first user I say hi to a second user
|
5
|
+
Given I wait
|
6
|
+
Then I send a signal to user 2 containing "hi"
|
7
|
+
|
8
|
+
@user2
|
9
|
+
Scenario: As a second user I wait for user 1 to say hi
|
10
|
+
Given I wait for a signal containing "hi"
|
11
|
+
Then I wait
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'kraken-mobile/runners/calabash/android/kraken_steps'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'calabash-android/management/app_installation'
|
2
|
+
|
3
|
+
AfterConfiguration do |config|
|
4
|
+
FeatureMemory.feature = nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Before do |scenario|
|
8
|
+
scenario = scenario.scenario_outline if scenario.respond_to?(:scenario_outline)
|
9
|
+
|
10
|
+
feature = scenario.feature
|
11
|
+
if FeatureMemory.feature != feature || ENV['RESET_BETWEEN_SCENARIOS'] == '1'
|
12
|
+
if ENV['RESET_BETWEEN_SCENARIOS'] == '1'
|
13
|
+
log 'New scenario - reinstalling apps'
|
14
|
+
else
|
15
|
+
log 'First scenario in feature - reinstalling apps'
|
16
|
+
end
|
17
|
+
clear_app_data
|
18
|
+
FeatureMemory.feature = feature
|
19
|
+
FeatureMemory.invocation = 1
|
20
|
+
else
|
21
|
+
FeatureMemory.invocation += 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
FeatureMemory = Struct.new(:feature, :invocation).new
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'kraken-mobile/runners/calabash/android/cucumber'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'kraken-mobile/runners/calabash/android/android_runner'
|
2
|
+
require 'kraken-mobile/runners/calabash/monkey/monkey_runner'
|
3
|
+
require 'kraken-mobile/constants'
|
4
|
+
|
5
|
+
module KrakenMobile
|
6
|
+
class App
|
7
|
+
# Constructors
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
@runner = current_runner
|
11
|
+
end
|
12
|
+
|
13
|
+
# Helpers
|
14
|
+
def run_in_parallel
|
15
|
+
@runner.run_in_parallel
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_runner
|
19
|
+
case @options[:runner]
|
20
|
+
when KrakenMobile::Constants::CALABASH_ANDROID
|
21
|
+
Runner::CalabashAndroidRunner.new(@options)
|
22
|
+
when KrakenMobile::Constants::MONKEY
|
23
|
+
Runner::MonkeyRunner.new(@options)
|
24
|
+
else
|
25
|
+
raise "Invalid Kraken runner."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module KrakenMobile
|
2
|
+
module Constants
|
3
|
+
# Runners
|
4
|
+
CALABASH_ANDROID = "calabash-android"
|
5
|
+
MONKEY = "monkey"
|
6
|
+
REPORT_PATH = "./reports"
|
7
|
+
REPORT_DEVICES_FILE_NAME = "devices"
|
8
|
+
REPORT_FILE_NAME = "report"
|
9
|
+
D3_DATA_FILE_NAME = "data"
|
10
|
+
|
11
|
+
# Protocols
|
12
|
+
FILE_PROTOCOL = "file-based"
|
13
|
+
SUPPORTED_PROTOCOLS = [FILE_PROTOCOL]
|
14
|
+
|
15
|
+
# Protocol
|
16
|
+
DEVICE_INBOX_NAME = "inbox"
|
17
|
+
KRAKEN_CONFIGURATION_FILE_NAME = "kraken_settings"
|
18
|
+
DEFAULT_TIMEOUT = 10
|
19
|
+
MONKEY_DEFAULT_TIMEOUT = 5
|
20
|
+
|
21
|
+
# ADB Orientations
|
22
|
+
PORTRAIT = 0
|
23
|
+
LANDSCAPE = 1
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module KrakenMobile
|
2
|
+
class CommandHelper
|
3
|
+
def user_is_using_windows
|
4
|
+
RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/
|
5
|
+
end
|
6
|
+
|
7
|
+
def terminal_command_separator
|
8
|
+
user_is_using_windows ? ' & ' : ';'
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_command commands
|
12
|
+
commands.compact*' '
|
13
|
+
end
|
14
|
+
|
15
|
+
# Exports a list of environment variables to the users computer.
|
16
|
+
def build_export_env_command env_variables
|
17
|
+
commands = env_variables.map { |key, value|
|
18
|
+
user_is_using_windows ? "(SET \"#{key}=#{value}\")" : "#{key}=#{value};export #{key}"
|
19
|
+
}
|
20
|
+
commands.join(terminal_command_separator)
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute_command process_number, command
|
24
|
+
output = open("|#{command}", 'r') { |output| show_output(output, process_number) }
|
25
|
+
exitstatus = $?.exitstatus
|
26
|
+
end
|
27
|
+
|
28
|
+
def show_output(output, process_number)
|
29
|
+
loop do
|
30
|
+
begin
|
31
|
+
line = output.readline()
|
32
|
+
$stdout.print "#{process_number}> #{line}"
|
33
|
+
$stdout.flush
|
34
|
+
end
|
35
|
+
end rescue EOFError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'kraken-mobile/models/device'
|
2
|
+
require 'kraken-mobile/constants'
|
3
|
+
|
4
|
+
module KrakenMobile
|
5
|
+
module DevicesHelper
|
6
|
+
class AdbHelper
|
7
|
+
# ADB command that returns all phones and emulators connected to the computer.
|
8
|
+
def adb_devices_l
|
9
|
+
`adb devices -l`
|
10
|
+
end
|
11
|
+
|
12
|
+
def file_content file_name, device_id
|
13
|
+
`adb -s #{device_id} shell "cat /sdcard/#{file_name} 2> /dev/null"`
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_content_to_device content, file_name, device_id
|
17
|
+
`adb -s #{device_id} shell "echo "#{content}" > /sdcard/#{file_name}"`
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_file_in_device file_name, device_id
|
21
|
+
`adb -s #{device_id} shell "> /sdcard/#{file_name}"`
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_file_in_device file_name, device_id
|
25
|
+
`adb -s #{device_id} shell "rm -rf /sdcard/#{file_name}"`
|
26
|
+
end
|
27
|
+
|
28
|
+
def device_screen_size device_id
|
29
|
+
`adb -s #{device_id} shell wm size`
|
30
|
+
end
|
31
|
+
|
32
|
+
def device_sdk_version device_id
|
33
|
+
`adb -s #{device_id} shell getprop ro.build.version.sdk`
|
34
|
+
end
|
35
|
+
|
36
|
+
def device_orientation device_id
|
37
|
+
`adb -s #{device_id} shell dumpsys input | grep 'SurfaceOrientation' | awk '{ print $2 }'`
|
38
|
+
end
|
39
|
+
|
40
|
+
def is_device_connected device_id
|
41
|
+
begin
|
42
|
+
adb_devices_l.include?(device_id)
|
43
|
+
rescue
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns an array with all the devices and emulators connected to the computer.
|
49
|
+
def connected_devices
|
50
|
+
begin
|
51
|
+
devices = []
|
52
|
+
list =
|
53
|
+
adb_devices_l.split("\n").each do |line|
|
54
|
+
line_id = extract_device_id(line)
|
55
|
+
line_model = extract_device_model(line)
|
56
|
+
if line_id && line_model
|
57
|
+
device = Models::Device.new(line_id, line_model, devices.size + 1)
|
58
|
+
devices << device
|
59
|
+
end
|
60
|
+
end
|
61
|
+
devices
|
62
|
+
rescue
|
63
|
+
[]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_file_content file_name, device_id
|
68
|
+
begin
|
69
|
+
raise "Device #{device_id} not found" unless is_device_connected(device_id)
|
70
|
+
content = file_content("#{file_name}.txt", device_id)
|
71
|
+
content.strip
|
72
|
+
rescue
|
73
|
+
""
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def write_content_to_file content, file_name, device_id
|
78
|
+
begin
|
79
|
+
raise "Device #{device_id} not found" unless is_device_connected(device_id)
|
80
|
+
write_content_to_device(content, "#{file_name}.txt", device_id)
|
81
|
+
true
|
82
|
+
rescue
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_file file_name, device_id
|
88
|
+
begin
|
89
|
+
raise "Device #{device_id} not found" unless is_device_connected(device_id)
|
90
|
+
create_file_in_device("#{file_name}.txt", device_id)
|
91
|
+
true
|
92
|
+
rescue
|
93
|
+
false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete_file file_name, device_id
|
98
|
+
begin
|
99
|
+
raise "Device #{device_id} not found" unless is_device_connected(device_id)
|
100
|
+
delete_file_in_device("#{file_name}.txt", device_id)
|
101
|
+
true
|
102
|
+
rescue
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns height, width
|
108
|
+
def screen_size device_id
|
109
|
+
begin
|
110
|
+
adb_size = device_screen_size device_id
|
111
|
+
parts = adb_size.strip!.split(" ")
|
112
|
+
size = parts[parts.count-1]
|
113
|
+
return 0,0 if !size.include?("x")
|
114
|
+
size_parts = size.split("x")
|
115
|
+
if orientation(device_id) == KrakenMobile::Constants::PORTRAIT
|
116
|
+
return size_parts[1].to_i, size_parts[0].to_i
|
117
|
+
else
|
118
|
+
return size_parts[0].to_i, size_parts[1].to_i
|
119
|
+
end
|
120
|
+
rescue
|
121
|
+
return 0,0
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def sdk_version device_id
|
126
|
+
begin
|
127
|
+
return device_sdk_version device_id
|
128
|
+
rescue
|
129
|
+
return "N/A"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def orientation device_id
|
134
|
+
begin
|
135
|
+
adb_orientation = device_orientation(device_id).strip!
|
136
|
+
return adb_orientation.to_i
|
137
|
+
rescue
|
138
|
+
return KrakenMobile::Constants::PORTRAIT
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Parses the device id from the ADB devices command.
|
143
|
+
def extract_device_id line
|
144
|
+
if line.match(/device(?!s)/)
|
145
|
+
line.split(" ").first
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Parses the device model from the ADB devices command.
|
150
|
+
def extract_device_model line
|
151
|
+
if line.match(/device(?!s)/)
|
152
|
+
line.scan(/model:(.*) device/).flatten.first
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'kraken-mobile/models/device'
|
2
|
+
require 'kraken-mobile/helpers/devices_helper/adb_helper'
|
3
|
+
require 'kraken-mobile/constants'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module KrakenMobile
|
7
|
+
module DevicesHelper
|
8
|
+
class Manager
|
9
|
+
def initialize(options)
|
10
|
+
@runner_name = options[:runner]
|
11
|
+
@config_path = options[:config_path]
|
12
|
+
end
|
13
|
+
|
14
|
+
def connected_devices
|
15
|
+
if @config_path
|
16
|
+
raise "The path of the configuration file is not valid" unless File.exist?(@config_path) && File.file?(@config_path) && @config_path.end_with?(".json")
|
17
|
+
file = open(@config_path)
|
18
|
+
content = file.read
|
19
|
+
configured_devices = JSON.parse(content)
|
20
|
+
devices = []
|
21
|
+
configured_devices.each do |dev_data|
|
22
|
+
device = Models::Device.new(dev_data["id"], dev_data["model"], devices.size + 1, dev_data["config"])
|
23
|
+
devices << device
|
24
|
+
end
|
25
|
+
devices
|
26
|
+
else
|
27
|
+
device_helper.connected_devices
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def device_helper
|
32
|
+
case @runner_name
|
33
|
+
when KrakenMobile::Constants::CALABASH_ANDROID
|
34
|
+
DevicesHelper::AdbHelper.new()
|
35
|
+
when KrakenMobile::Constants::MONKEY
|
36
|
+
DevicesHelper::AdbHelper.new()
|
37
|
+
else
|
38
|
+
raise "Runner is not supported"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'gherkin/parser'
|
2
|
+
require 'gherkin/pickles/compiler'
|
3
|
+
|
4
|
+
def ensure_features_format files
|
5
|
+
files.each do |file_path|
|
6
|
+
ensure_feature_has_unique_tags file_path
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def ensure_feature_has_unique_tags file_path
|
11
|
+
parser = Gherkin::Parser.new
|
12
|
+
file = open(file_path)
|
13
|
+
content = file.read
|
14
|
+
gherkin_document = parser.parse(content)
|
15
|
+
pickles = Gherkin::Pickles::Compiler.new.compile(gherkin_document)
|
16
|
+
tag_hash = {}
|
17
|
+
pickles.each do |scenario|
|
18
|
+
raise "Scenario '#{scenario[:name]}' can't have more than one @user{int} tag." if scenario[:tags].select{ |tag| tag[:name].start_with? "@user" }.count > 1
|
19
|
+
scenario[:tags].each do |tag|
|
20
|
+
tag_name = tag[:name]
|
21
|
+
if tag_hash[tag_name]
|
22
|
+
raise "Tag #{tag_name} is duplicated. Each feature can only have one @user{:int} tag assigned to a scenario."
|
23
|
+
else
|
24
|
+
tag_hash[tag_name] = tag_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'kraken-mobile/helpers/feature_analyzer'
|
3
|
+
|
4
|
+
module KrakenMobile
|
5
|
+
class FeatureGrouper
|
6
|
+
# Returns files in feature_folder distributed equally in group size.
|
7
|
+
def self.distributed_file_groups(feature_folder, group_size)
|
8
|
+
files = feature_files_in_folder feature_folder
|
9
|
+
groups = create_file_groups group_size,files
|
10
|
+
groups
|
11
|
+
end
|
12
|
+
|
13
|
+
# All groups contains all files in feature_folder
|
14
|
+
def self.file_groups(feature_folder, group_size)
|
15
|
+
files = feature_files_in_folder feature_folder
|
16
|
+
ensure_features_format files
|
17
|
+
group_size.times.map { files }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.create_file_groups group_size, files
|
21
|
+
files_per_group = files.size/group_size
|
22
|
+
number_of_remaining_files = files.size % group_size
|
23
|
+
groups = Array.new(group_size) { [] }
|
24
|
+
groups.each do |group|
|
25
|
+
files_per_group.times {
|
26
|
+
group << files.delete_at(0)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
unless number_of_remaining_files == 0
|
30
|
+
groups[0..(number_of_remaining_files-1)].each do |group|
|
31
|
+
group << files.delete_at(0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
groups.reject(&:empty?)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.feature_files_in_folder(feature_dir_or_file)
|
38
|
+
if File.directory?(feature_dir_or_file) # Is a folder containing feature files.
|
39
|
+
files = Dir[File.join(feature_dir_or_file, "**{,/*/**}/*")].uniq
|
40
|
+
files.grep(/\.feature$/)
|
41
|
+
elsif feature_dir_or_file.include?('.feature') # Is a feature file.
|
42
|
+
[feature_dir_or_file]
|
43
|
+
else
|
44
|
+
[]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,381 @@
|
|
1
|
+
require 'kraken-mobile/constants'
|
2
|
+
require 'digest'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module KrakenMobile
|
6
|
+
class Reporter
|
7
|
+
|
8
|
+
PASSED = 'passed'
|
9
|
+
FAILED = 'failed',
|
10
|
+
SKIPPED = 'skipped',
|
11
|
+
PENDING = 'pending',
|
12
|
+
NOT_DEFINED = 'undefined',
|
13
|
+
AMBIGUOUS = 'ambiguous'
|
14
|
+
|
15
|
+
def initialize(execution_id, options)
|
16
|
+
@execution_id = execution_id
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
#-------------------------------
|
21
|
+
# Generator
|
22
|
+
#-------------------------------
|
23
|
+
def generate_general_report
|
24
|
+
erb_file = File.join(File.expand_path("../../../../reporter/", __FILE__), "index.html.erb")
|
25
|
+
html_file = File.join(File.expand_path("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/"), "index.html")
|
26
|
+
# Variables
|
27
|
+
report_file = open("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{KrakenMobile::Constants::REPORT_DEVICES_FILE_NAME}.json")
|
28
|
+
content = report_file.read
|
29
|
+
@devices = JSON.parse(content)
|
30
|
+
devices_report = report_by_devices(@devices)
|
31
|
+
@features_report = fetures_from_report_by_devices devices_report
|
32
|
+
data_hash = feature_by_nodes_and_links @features_report
|
33
|
+
file = open("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/assets/js/#{KrakenMobile::Constants::D3_DATA_FILE_NAME}.json", 'w')
|
34
|
+
file.puts(data_hash.to_json)
|
35
|
+
file.close
|
36
|
+
template = File.read(erb_file)
|
37
|
+
result = ERB.new(template).result(binding)
|
38
|
+
# write result to file
|
39
|
+
File.open(html_file, 'w+') do |f|
|
40
|
+
f.write result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate_device_report device
|
45
|
+
@apk_path = device.config["apk_path"] ? device.config["apk_path"] : @options[:apk_path]
|
46
|
+
report_file = open("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device.id}/#{KrakenMobile::Constants::REPORT_FILE_NAME}.json")
|
47
|
+
content = report_file.read
|
48
|
+
@features = JSON.parse(content)
|
49
|
+
@total_scenarios = total_scenarios @features
|
50
|
+
@device = device
|
51
|
+
@total_failed_scenarios_percentage = total_failed_scenarios_percentage @features
|
52
|
+
@total_passed_scenarios_percentage = total_passed_scenarios_percentage @features
|
53
|
+
@total_passed_features_percentage = total_passed_features_percentage @features
|
54
|
+
@total_failed_features_percentage = total_failed_features_percentage @features
|
55
|
+
erb_file = File.join(File.expand_path("../../../../reporter/", __FILE__), "feature_report.html.erb")
|
56
|
+
html_file = File.join(File.expand_path("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device.id}/"), File.basename(erb_file, '.erb')) #=>"page.html"
|
57
|
+
# Variables
|
58
|
+
template = File.read(erb_file)
|
59
|
+
result = ERB.new(template).result(binding)
|
60
|
+
# write result to file
|
61
|
+
File.open(html_file, 'w+') do |f|
|
62
|
+
f.write result
|
63
|
+
end
|
64
|
+
generate_features_report @features, device
|
65
|
+
end
|
66
|
+
|
67
|
+
def report_by_devices devices
|
68
|
+
devices_report = {}
|
69
|
+
devices.each do |device|
|
70
|
+
next if !File.exists?("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device['id']}/#{KrakenMobile::Constants::REPORT_FILE_NAME}.json")
|
71
|
+
report_file = open("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device['id']}/#{KrakenMobile::Constants::REPORT_FILE_NAME}.json")
|
72
|
+
content = report_file.read
|
73
|
+
devices_report[device['user']] = JSON.parse(content)
|
74
|
+
devices_report[device['user']].each do |d| d["device_model"] = device["model"] if !d["device_model"] end
|
75
|
+
devices_report[device['user']].each do |d| d["device_id"] = device["id"] if !d["device_id"] end
|
76
|
+
end
|
77
|
+
devices_report
|
78
|
+
end
|
79
|
+
|
80
|
+
def fetures_from_report_by_devices report_by_devices
|
81
|
+
features = {}
|
82
|
+
report_by_devices.keys.each do |user_key|
|
83
|
+
report = report_by_devices[user_key]
|
84
|
+
report.each do |feature|
|
85
|
+
features[feature["id"]] = {} if !features[feature["id"]]
|
86
|
+
features[feature["id"]]["name"] = feature["name"] if !features[feature["id"]]["name"] && feature["name"]
|
87
|
+
features[feature["id"]]["devices"] = {} if !features[feature["id"]]["devices"]
|
88
|
+
if feature["elements"] && feature["elements"].count > 0
|
89
|
+
features[feature["id"]]["devices"][user_key] = []
|
90
|
+
if feature["elements"].first["steps"]
|
91
|
+
failed = false
|
92
|
+
feature["elements"].first["steps"].each do |step|
|
93
|
+
next if failed
|
94
|
+
failed = step["result"]["status"] != PASSED
|
95
|
+
image = nil
|
96
|
+
image = step["after"].first["embeddings"].first["data"] if step["after"] && step["after"].count > 0 && step["after"].first["embeddings"] && step["after"].first["embeddings"].count > 0
|
97
|
+
features[feature["id"]]["devices"][user_key] << {
|
98
|
+
name: "#{step['keyword']} #{step['name']}",
|
99
|
+
duration: step["result"]["duration"],
|
100
|
+
image: image,
|
101
|
+
device_model: feature["device_model"],
|
102
|
+
status: failed ? FAILED : PASSED
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
features
|
110
|
+
end
|
111
|
+
|
112
|
+
def feature_by_nodes_and_links features_report
|
113
|
+
features = []
|
114
|
+
features_report.values.each do |feature|
|
115
|
+
features << nodes_and_links(feature["devices"], feature["name"]) if feature["devices"]
|
116
|
+
end
|
117
|
+
features
|
118
|
+
end
|
119
|
+
|
120
|
+
def nodes_and_links feature_report, feature_name
|
121
|
+
last_node_id = 0
|
122
|
+
nodes = [{ name: "", id: "empty", image: nil }]
|
123
|
+
signal_hash = {}
|
124
|
+
links = []
|
125
|
+
feature_report.keys.each do |key|
|
126
|
+
steps = feature_report[key]
|
127
|
+
coming_from_signal = false
|
128
|
+
last_signal = -1
|
129
|
+
steps.each_with_index do |step, index|
|
130
|
+
node_id = last_node_id+1
|
131
|
+
if isReadSignal(step[:name]) && step[:status] == PASSED
|
132
|
+
signal = signalContent(step[:name])
|
133
|
+
already_created_signal = signal_hash[signal] ? true : false
|
134
|
+
signal_hash[signal] = already_created_signal ? signal_hash[signal] : { id: "#{node_id}", receiver: key }
|
135
|
+
node = { name: "Signal: #{signal}, Receiver: #{step[:device_model]}", id: signal_hash[signal][:id], image: nil, status: step[:status] }
|
136
|
+
if already_created_signal
|
137
|
+
entry = nodes.select{ |node| node[:id] == signal_hash[signal][:id] }.first
|
138
|
+
entry[:name] = "Signal: #{signal}, Receiver: #{step[:device_model]}" if entry
|
139
|
+
end
|
140
|
+
source = (coming_from_signal ? last_signal : (index == 0 ? 0 : last_node_id))
|
141
|
+
link = {
|
142
|
+
source: source,
|
143
|
+
target: signal_hash[signal][:id].to_i,
|
144
|
+
value: 1,
|
145
|
+
owner: key,
|
146
|
+
owner_model: step[:device_model]
|
147
|
+
}
|
148
|
+
nodes << node if !already_created_signal
|
149
|
+
links << link
|
150
|
+
last_node_id += 1 if !already_created_signal
|
151
|
+
last_signal = signal_hash[signal][:id].to_i
|
152
|
+
coming_from_signal = true
|
153
|
+
elsif isWriteSignal(step[:name]) && step[:status] == PASSED
|
154
|
+
signal = signalContent(step[:name])
|
155
|
+
receiver = signalReceiver(step[:name])
|
156
|
+
already_created_signal = signal_hash[signal] ? true : false
|
157
|
+
signal_hash[signal] = already_created_signal ? signal_hash[signal] : { id: "#{node_id}", receiver: receiver }
|
158
|
+
node = { name: step[:name], id: signal_hash[signal][:id], image: nil, status: step[:status] }
|
159
|
+
source = (coming_from_signal ? last_signal : (index == 0 ? 0 : last_node_id))
|
160
|
+
link = {
|
161
|
+
source: source,
|
162
|
+
target: signal_hash[signal][:id].to_i,
|
163
|
+
value: 1,
|
164
|
+
owner: key,
|
165
|
+
owner_model: step[:device_model]
|
166
|
+
}
|
167
|
+
nodes << node if !already_created_signal
|
168
|
+
links << link
|
169
|
+
last_node_id += 1 if !already_created_signal
|
170
|
+
last_signal = signal_hash[signal][:id].to_i
|
171
|
+
coming_from_signal = true
|
172
|
+
else
|
173
|
+
node = { name: step[:name], id: "#{node_id}", image: step[:image], status: step[:status] }
|
174
|
+
source = (coming_from_signal ? last_signal : (index == 0 ? 0 : last_node_id))
|
175
|
+
link = {
|
176
|
+
source: source,
|
177
|
+
target: node_id,
|
178
|
+
value: 1,
|
179
|
+
owner: key,
|
180
|
+
owner_model: step[:device_model]
|
181
|
+
}
|
182
|
+
nodes << node
|
183
|
+
links << link
|
184
|
+
last_node_id += 1
|
185
|
+
coming_from_signal = false
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
return {
|
190
|
+
name: feature_name,
|
191
|
+
nodes: nodes,
|
192
|
+
links: links
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
def isReadSignal step
|
197
|
+
line = step.split(' ')[1..-1].join(' ')
|
198
|
+
(line =~ /^I wait for a signal containing "([^\"]*)"$/ ? true : false) || (line =~ /^I wait for a signal containing "([^\"]*)" for (\d+) seconds$/ ? true : false)
|
199
|
+
end
|
200
|
+
|
201
|
+
def isWriteSignal step
|
202
|
+
line = step.split(' ')[1..-1].join(' ')
|
203
|
+
line =~ /^I send a signal to user (\d+) containing "([^\"]*)"$/ ? true : false
|
204
|
+
end
|
205
|
+
|
206
|
+
def signalContent step
|
207
|
+
line = step.split(' ')[1..-1].join(' ')
|
208
|
+
line.scan(/"([^\"]*)"/).first.first if line.scan(/"([^\"]*)"/).first
|
209
|
+
end
|
210
|
+
|
211
|
+
def signalReceiver step
|
212
|
+
line = step.split(' ')[1..-1].join(' ')
|
213
|
+
line.scan(/(\d+)/).first.first if line.scan(/(\d+)/).first
|
214
|
+
end
|
215
|
+
|
216
|
+
def generate_features_report features, device
|
217
|
+
features.each do |feature|
|
218
|
+
generate_feature_report feature, device
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def generate_feature_report feature, device
|
223
|
+
Dir.mkdir("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device.id}/features_report") unless File.exists?("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device.id}/features_report")
|
224
|
+
file_name = feature_id feature
|
225
|
+
erb_file = File.join(File.expand_path("../../../../reporter/", __FILE__), "scenario_report.html.erb")
|
226
|
+
html_file = File.join(File.expand_path("#{KrakenMobile::Constants::REPORT_PATH}/#{@execution_id}/#{device.id}/features_report"), "#{file_name}.html") #=>"page.html"
|
227
|
+
# Variables
|
228
|
+
@feature = feature
|
229
|
+
template = File.read(erb_file)
|
230
|
+
result = ERB.new(template).result(binding)
|
231
|
+
# write result to file
|
232
|
+
File.open(html_file, 'w+') do |f|
|
233
|
+
f.write result
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# 0: create 1: commit 2: merge
|
238
|
+
def branches features_report
|
239
|
+
branches = {}
|
240
|
+
features_report.keys.each do |key|
|
241
|
+
report = features_report[key]
|
242
|
+
branches[report["hash"]] = {} if !branches[report["hash"]]
|
243
|
+
branches[report["hash"]]["steps"]= [] if !branches[report["hash"]]["steps"]
|
244
|
+
devices = report["devices"]
|
245
|
+
devices.keys.each do |device_key|
|
246
|
+
branches[report["hash"]]["steps"] << { type: 0, name: device_key }
|
247
|
+
feature_steps = devices[device_key][0]["steps"] if devices[device_key].count > 0 && devices[device_key][0]["steps"]
|
248
|
+
feature_steps.each do |step|
|
249
|
+
hash_step = { type: 1, name: device_key}
|
250
|
+
hash_step[:image] = step["after"][0]["embeddings"][0] if step["after"].count > 0 && step["after"][0]["embeddings"] && step["after"][0]["embeddings"].count > 0
|
251
|
+
branches[report["hash"]]["steps"] << hash_step
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
branches
|
256
|
+
end
|
257
|
+
|
258
|
+
#-------------------------------
|
259
|
+
# Helpers
|
260
|
+
#-------------------------------
|
261
|
+
def total_scenarios features
|
262
|
+
how_many = 0
|
263
|
+
features.each do |feature|
|
264
|
+
scenarios = feature["elements"]
|
265
|
+
how_many += scenarios.count if scenarios
|
266
|
+
end
|
267
|
+
how_many
|
268
|
+
end
|
269
|
+
|
270
|
+
def feature_id feature
|
271
|
+
Digest::SHA256.hexdigest("#{feature["id"].strip}#{feature["uri"].strip}")
|
272
|
+
end
|
273
|
+
|
274
|
+
def passed_features features
|
275
|
+
features.select{ |feature| passed_scenarios(feature) == feature["elements"].count }
|
276
|
+
end
|
277
|
+
|
278
|
+
def failed_features features
|
279
|
+
features.select{ |feature| failed_scenarios(feature) == feature["elements"].count }
|
280
|
+
end
|
281
|
+
|
282
|
+
def feature_duration feature
|
283
|
+
scenarios = feature["elements"]
|
284
|
+
how_long = 0
|
285
|
+
scenarios.each do |scenario|
|
286
|
+
how_long += scenario_duration(scenario)
|
287
|
+
end
|
288
|
+
how_long
|
289
|
+
end
|
290
|
+
|
291
|
+
def scenario_duration scenario
|
292
|
+
how_long = 0
|
293
|
+
scenario["steps"].each do |step|
|
294
|
+
how_long += step["result"]["duration"] if step["result"] && step["result"]["duration"]
|
295
|
+
end
|
296
|
+
how_long
|
297
|
+
end
|
298
|
+
|
299
|
+
def passed_scenarios feature
|
300
|
+
scenarios = feature["elements"]
|
301
|
+
scenarios.select{ |scenario|
|
302
|
+
steps = scenario["steps"]
|
303
|
+
steps.all?{ |step| step["result"] && step["result"]["status"] == PASSED }
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
def failed_scenarios feature
|
308
|
+
scenarios = feature["elements"]
|
309
|
+
scenarios.select{ |scenario|
|
310
|
+
steps = scenario["steps"]
|
311
|
+
steps.any?{ |step| step["result"] && step["result"]["status"] != PASSED }
|
312
|
+
}
|
313
|
+
end
|
314
|
+
|
315
|
+
def total_passed_scenarios features
|
316
|
+
how_many = 0
|
317
|
+
features.each do |feature|
|
318
|
+
how_many += passed_scenarios(feature).count
|
319
|
+
end
|
320
|
+
how_many
|
321
|
+
end
|
322
|
+
|
323
|
+
def total_failed_scenarios features
|
324
|
+
how_many = 0
|
325
|
+
features.each do |feature|
|
326
|
+
how_many += failed_scenarios(feature).count
|
327
|
+
end
|
328
|
+
how_many
|
329
|
+
end
|
330
|
+
|
331
|
+
def total_passed_features features
|
332
|
+
how_many = 0
|
333
|
+
features.each do |feature|
|
334
|
+
how_many += 1 if feature_passed?(feature)
|
335
|
+
end
|
336
|
+
how_many
|
337
|
+
end
|
338
|
+
|
339
|
+
def total_failed_features features
|
340
|
+
how_many = 0
|
341
|
+
features.each do |feature|
|
342
|
+
how_many += 1 if !feature_passed?(feature)
|
343
|
+
end
|
344
|
+
how_many
|
345
|
+
end
|
346
|
+
|
347
|
+
def feature_passed_scenarios_percentage feature
|
348
|
+
(passed_scenarios(feature).count.to_f/feature["elements"].count.to_f).round(2) * 100.00
|
349
|
+
end
|
350
|
+
|
351
|
+
def feature_failed_scenarios_percentage feature
|
352
|
+
(failed_scenarios(feature).count.to_f/feature["elements"].count.to_f).round(2) * 100.00
|
353
|
+
end
|
354
|
+
|
355
|
+
def total_passed_scenarios_percentage features
|
356
|
+
(total_passed_scenarios(features).to_f/total_scenarios(features).to_f).round(2) * 100.00
|
357
|
+
end
|
358
|
+
|
359
|
+
def total_passed_features_percentage features
|
360
|
+
(total_passed_features(features).to_f/features.count.to_f).round(2) * 100.00
|
361
|
+
end
|
362
|
+
|
363
|
+
def total_failed_scenarios_percentage features
|
364
|
+
(total_failed_scenarios(features).to_f/total_scenarios(features).to_f).round(2) * 100.00
|
365
|
+
end
|
366
|
+
|
367
|
+
def total_failed_features_percentage features
|
368
|
+
(total_failed_features(features).to_f/features.count.to_f).round(2) * 100.00
|
369
|
+
end
|
370
|
+
|
371
|
+
def feature_passed? feature
|
372
|
+
passed_scenarios(feature).count == feature["elements"].count
|
373
|
+
end
|
374
|
+
|
375
|
+
def format_duration(nanoseconds)
|
376
|
+
duration_in_seconds = nanoseconds.to_f/1000000000.0
|
377
|
+
m, s = duration_in_seconds.divmod(60)
|
378
|
+
"#{m}m #{format('%.3f', s)}s"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|