kraken-mobile 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +45 -11
- data/bin/kraken-mobile +55 -67
- data/bin/kraken_mobile_calabash_android.rb +39 -0
- data/bin/kraken_mobile_helpers.rb +91 -0
- data/bin/kraken_mobile_setup.rb +134 -0
- data/calabash-android-features-skeleton/step_definitions/mobile_steps.rb +1 -0
- data/calabash-android-features-skeleton/support/app_installation_hooks.rb +2 -0
- data/calabash-android-features-skeleton/support/app_life_cycle_hooks.rb +3 -4
- data/calabash-android-features-skeleton/support/env.rb +1 -1
- data/calabash-android-features-skeleton/web/step_definitions/web_steps.rb +3 -0
- data/calabash-android-features-skeleton/web/support/app_life_cycle_hooks.rb +15 -0
- data/lib/kraken-mobile/device_process.rb +94 -0
- data/lib/kraken-mobile/helpers/devices_helper/adb_helper.rb +100 -105
- data/lib/kraken-mobile/helpers/kraken_faker.rb +107 -0
- data/lib/kraken-mobile/hooks/mobile_kraken_hooks.rb +15 -0
- data/lib/kraken-mobile/hooks/mobile_operations.rb +36 -0
- data/lib/kraken-mobile/hooks/web_operations.rb +33 -0
- data/lib/kraken-mobile/mobile/adb.rb +66 -0
- data/lib/kraken-mobile/mobile/android_commands.rb +43 -0
- data/lib/kraken-mobile/mobile/mobile_process.rb +82 -0
- data/lib/kraken-mobile/models/android_device.rb +121 -0
- data/lib/kraken-mobile/models/device.rb +113 -31
- data/lib/kraken-mobile/models/feature_file.rb +108 -0
- data/lib/kraken-mobile/models/feature_scenario.rb +24 -0
- data/lib/kraken-mobile/models/web_device.rb +86 -0
- data/lib/kraken-mobile/monkeys/mobile/android_monkey.rb +30 -0
- data/lib/kraken-mobile/monkeys/mobile/kraken_android_monkey.rb +54 -0
- data/lib/kraken-mobile/runners/calabash/android/steps/communication_steps.rb +15 -0
- data/lib/kraken-mobile/steps/general_steps.rb +82 -0
- data/lib/kraken-mobile/steps/mobile/kraken_steps.rb +72 -0
- data/lib/kraken-mobile/steps/web/kraken_steps.rb +81 -0
- data/lib/kraken-mobile/test_scenario.rb +193 -0
- data/lib/kraken-mobile/utils/feature_reader.rb +17 -0
- data/lib/kraken-mobile/utils/k.rb +67 -0
- data/lib/kraken-mobile/utils/mobile_cucumber.rb +2 -0
- data/lib/kraken-mobile/utils/reporter.rb +453 -0
- data/lib/kraken-mobile/version.rb +2 -2
- data/lib/kraken-mobile/web/web_process.rb +39 -0
- data/lib/kraken_mobile.rb +77 -0
- data/reporter/index.html.erb +6 -6
- metadata +89 -9
- data/bin/kraken-mobile-calabash-android.rb +0 -85
- data/bin/kraken-mobile-generate.rb +0 -19
- data/bin/kraken-mobile-helpers.rb +0 -48
- data/bin/kraken-mobile-setup.rb +0 -50
- data/calabash-android-features-skeleton/step_definitions/kraken_steps.rb +0 -1
- data/lib/kraken-mobile.rb +0 -29
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'selenium-webdriver'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
driver = Selenium::WebDriver.for :chrome
|
5
|
+
|
6
|
+
Given(/^I navigate to page "([^\"]*)"$/) do |web_url|
|
7
|
+
raise 'ERROR: Invalid URL' if web_url.nil?
|
8
|
+
raise 'ERROR: Invalid URL' unless web_url =~ URI::DEFAULT_PARSER.make_regexp
|
9
|
+
|
10
|
+
driver.navigate.to web_url
|
11
|
+
sleep 2
|
12
|
+
end
|
13
|
+
|
14
|
+
Then(/^I enter "([^\"]*)" into input field having id "([^\"]*)"$/) do |text, id|
|
15
|
+
driver.find_element(:id, id).send_keys(text)
|
16
|
+
sleep 2
|
17
|
+
end
|
18
|
+
|
19
|
+
Then(
|
20
|
+
/^I enter "([^\"]*)" into input field having css selector "([^\"]*)"$/
|
21
|
+
) do |text, selector|
|
22
|
+
driver.find_element(:css, selector).send_keys(text)
|
23
|
+
sleep 2
|
24
|
+
end
|
25
|
+
|
26
|
+
Then(/^I click on element having id "(.*?)"$/) do |id|
|
27
|
+
driver.find_element(:id, id).click
|
28
|
+
sleep 2
|
29
|
+
end
|
30
|
+
|
31
|
+
Then(/^I wait for (\d+) seconds$/) do |seconds|
|
32
|
+
return if seconds.nil?
|
33
|
+
|
34
|
+
sleep seconds.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
Then(/^I should see text "(.*?)"$/) do |text|
|
38
|
+
driver.page_source.include?(text)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Kraken Steps
|
42
|
+
Then(
|
43
|
+
/^I send a signal to user (\d+) containing "([^\"]*)"$/
|
44
|
+
) do |process_id, signal|
|
45
|
+
device = Device.find_by_process_id(process_id)
|
46
|
+
raise 'ERROR: Device not found' if device.nil?
|
47
|
+
if process_id.to_s == current_process_id.to_s
|
48
|
+
raise 'ERROR: Can\'t send signal to same device'
|
49
|
+
end
|
50
|
+
|
51
|
+
device.write_signal(signal)
|
52
|
+
end
|
53
|
+
|
54
|
+
Then(/^I wait for a signal containing "([^\"]*)"$/) do |signal|
|
55
|
+
raise 'ERROR: Invalid scenario tag' if @scenario_tags.nil?
|
56
|
+
raise 'ERROR: Invalid scenario tag' if @scenario_tags.grep(/@user/).none?
|
57
|
+
|
58
|
+
device = Device.find_by_process_id(current_process_id)
|
59
|
+
raise 'ERROR: Device not found' if device.nil?
|
60
|
+
|
61
|
+
device.read_signal(signal)
|
62
|
+
end
|
63
|
+
|
64
|
+
Then(
|
65
|
+
/^I wait for a signal containing "([^\"]*)" for (\d+) seconds$/
|
66
|
+
) do |signal, seconds|
|
67
|
+
raise 'ERROR: Invalid scenario tag' if @scenario_tags.nil?
|
68
|
+
raise 'ERROR: Invalid scenario tag' if @scenario_tags.grep(/@user/).none?
|
69
|
+
|
70
|
+
device = Device.find_by_process_id(current_process_id)
|
71
|
+
raise 'ERROR: Device not found' if device.nil?
|
72
|
+
|
73
|
+
device.read_signal(signal, seconds)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def current_process_id
|
79
|
+
tag_process_id = @scenario_tags.grep(/@user/).first
|
80
|
+
tag_process_id.delete_prefix('@user')
|
81
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'kraken-mobile/helpers/feature_analyzer'
|
2
|
+
require 'kraken-mobile/mobile/mobile_process'
|
3
|
+
require 'kraken-mobile/models/feature_file'
|
4
|
+
require 'kraken-mobile/models/web_device'
|
5
|
+
require 'kraken-mobile/web/web_process'
|
6
|
+
require 'kraken-mobile/utils/reporter'
|
7
|
+
require 'kraken-mobile/mobile/adb'
|
8
|
+
require 'kraken-mobile/utils/k'
|
9
|
+
require 'parallel'
|
10
|
+
|
11
|
+
class TestScenario
|
12
|
+
#-------------------------------
|
13
|
+
# Fields
|
14
|
+
#-------------------------------
|
15
|
+
attr_accessor :devices
|
16
|
+
attr_accessor :kraken_app
|
17
|
+
attr_accessor :feature_file
|
18
|
+
attr_accessor :execution_id
|
19
|
+
attr_accessor :reporter
|
20
|
+
|
21
|
+
#-------------------------------
|
22
|
+
# Constructors
|
23
|
+
#-------------------------------
|
24
|
+
def initialize(kraken_app:, feature_file_path:)
|
25
|
+
@feature_file = FeatureFile.new(file_path: feature_file_path)
|
26
|
+
@devices = sample_devices
|
27
|
+
@kraken_app = kraken_app
|
28
|
+
@execution_id = Digest::SHA256.hexdigest(Time.now.to_f.to_s)
|
29
|
+
@reporter = Reporter.new(test_scenario: self)
|
30
|
+
end
|
31
|
+
|
32
|
+
#-------------------------------
|
33
|
+
# Lifecycle
|
34
|
+
#-------------------------------
|
35
|
+
def before_execution
|
36
|
+
delete_all_web_inboxes
|
37
|
+
File.delete(K::DIRECTORY_PATH) if File.exist?(K::DIRECTORY_PATH)
|
38
|
+
File.delete(K::DICTIONARY_PATH) if File.exist?(K::DICTIONARY_PATH)
|
39
|
+
K::PROCESS_STATE_FILE_PATH.each do |_state, file_path|
|
40
|
+
File.delete(file_path) if File.exist?(file_path)
|
41
|
+
end
|
42
|
+
@reporter.create_report_folder_requirements
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
unless @feature_file.right_syntax?
|
47
|
+
raise "ERROR: Verify feature file #{@file_path} has one unique @user tag"\
|
48
|
+
' for each scenario'
|
49
|
+
end
|
50
|
+
|
51
|
+
before_execution
|
52
|
+
execute
|
53
|
+
after_execution
|
54
|
+
end
|
55
|
+
|
56
|
+
def after_execution
|
57
|
+
File.delete(K::DIRECTORY_PATH) if File.exist?(K::DIRECTORY_PATH)
|
58
|
+
File.delete(K::DICTIONARY_PATH) if File.exist?(K::DICTIONARY_PATH)
|
59
|
+
K::PROCESS_STATE_FILE_PATH.each do |_state, file_path|
|
60
|
+
File.delete(file_path) if File.exist?(file_path)
|
61
|
+
end
|
62
|
+
@reporter.save_report
|
63
|
+
notify_scenario_finished
|
64
|
+
end
|
65
|
+
|
66
|
+
#-------------------------------
|
67
|
+
# Methods
|
68
|
+
#-------------------------------
|
69
|
+
def execute
|
70
|
+
Parallel.map_with_index(
|
71
|
+
@devices, in_threads: @devices.count
|
72
|
+
) do |device, index|
|
73
|
+
user_id = index + 1
|
74
|
+
start_process_for_user_id_in_device(
|
75
|
+
user_id,
|
76
|
+
device
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.ready_to_start?
|
82
|
+
process_ids = DeviceProcess.registered_process_ids
|
83
|
+
processes_ready = DeviceProcess.processes_in_state(
|
84
|
+
K::PROCESS_STATES[:ready_to_start]
|
85
|
+
)
|
86
|
+
process_ids.all? do |process_id|
|
87
|
+
processes_ready.include?(process_id) ||
|
88
|
+
Device.find_by_process_id(process_id)&.connected? == false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.ready_to_finish?
|
93
|
+
process_ids = DeviceProcess.registered_process_ids
|
94
|
+
processes_finished = DeviceProcess.processes_in_state(
|
95
|
+
K::PROCESS_STATES[:ready_to_finish]
|
96
|
+
)
|
97
|
+
process_ids.all? do |process_id|
|
98
|
+
processes_finished.include?(process_id) ||
|
99
|
+
Device.find_by_process_id(process_id)&.connected? == false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def requires_predefined_devices?
|
104
|
+
!ENV[K::CONFIG_PATH].nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def start_process_for_user_id_in_device(user_id, device)
|
110
|
+
if device.is_a? AndroidDevice
|
111
|
+
start_mobile_process_for_user_id_in_device(user_id, device)
|
112
|
+
elsif device.is_a? WebDevice
|
113
|
+
start_web_process_for_user_id_in_device(user_id, device)
|
114
|
+
else
|
115
|
+
raise 'ERROR: Platform not supported'
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def start_mobile_process_for_user_id_in_device(user_id, device)
|
120
|
+
MobileProcess.new(
|
121
|
+
id: user_id,
|
122
|
+
device: device,
|
123
|
+
test_scenario: self
|
124
|
+
).run
|
125
|
+
end
|
126
|
+
|
127
|
+
def start_web_process_for_user_id_in_device(user_id, device)
|
128
|
+
WebProcess.new(
|
129
|
+
id: user_id,
|
130
|
+
device: device,
|
131
|
+
test_scenario: self
|
132
|
+
).run
|
133
|
+
end
|
134
|
+
|
135
|
+
def sample_devices
|
136
|
+
return predefined_devices if requires_predefined_devices?
|
137
|
+
|
138
|
+
(sample_mobile_devices + sample_web_devices).flatten
|
139
|
+
end
|
140
|
+
|
141
|
+
def sample_mobile_devices
|
142
|
+
android_devices = ADB.connected_devices
|
143
|
+
android_devices.sample(
|
144
|
+
@feature_file.number_of_required_mobile_devices
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
def sample_web_devices
|
149
|
+
web_devices = []
|
150
|
+
@feature_file.number_of_required_web_devices.times do
|
151
|
+
web_devices << WebDevice.factory_create
|
152
|
+
end
|
153
|
+
web_devices
|
154
|
+
end
|
155
|
+
|
156
|
+
def notify_scenario_finished
|
157
|
+
@kraken_app.on_test_scenario_finished
|
158
|
+
end
|
159
|
+
|
160
|
+
def user_id_is_mobile?(user_id)
|
161
|
+
complement_tags = @feature_file.tags_for_user_id(user_id).map(
|
162
|
+
&:downcase
|
163
|
+
)
|
164
|
+
complement_tags.include?('@mobile')
|
165
|
+
end
|
166
|
+
|
167
|
+
def user_id_is_web?(user_id)
|
168
|
+
complement_tags = @feature_file.tags_for_user_id(user_id).map(
|
169
|
+
&:downcase
|
170
|
+
)
|
171
|
+
complement_tags.include?('@web')
|
172
|
+
end
|
173
|
+
|
174
|
+
def delete_all_web_inboxes
|
175
|
+
Dir.glob(".*_#{K::INBOX_FILE_NAME}").each do |file|
|
176
|
+
File.delete(file)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def predefined_devices
|
181
|
+
config_absolute_path = File.expand_path(ENV[K::CONFIG_PATH])
|
182
|
+
file = open(config_absolute_path)
|
183
|
+
content = file.read
|
184
|
+
devices_json = JSON.parse(content).values
|
185
|
+
|
186
|
+
devices_json.map do |device_json|
|
187
|
+
AndroidDevice.new(
|
188
|
+
id: device_json['id'],
|
189
|
+
model: device_json['model']
|
190
|
+
)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'kraken-mobile/models/feature_file'
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
module FeatureReader
|
5
|
+
def feature_files
|
6
|
+
features_dir = File.join(FileUtils.pwd, K::FEATURES_PATH)
|
7
|
+
unless File.exist?(features_dir)
|
8
|
+
raise "ERROR: File or directory '#{features_dir}' does not exists"
|
9
|
+
end
|
10
|
+
# Is a file not directory
|
11
|
+
return [features_dir] if features_dir.include?('.feature')
|
12
|
+
|
13
|
+
files = Dir[File.join(features_dir, '**{,/*/**}/*')].uniq
|
14
|
+
files.grep(/\.feature$/)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K
|
4
|
+
SEPARATOR = ';' unless defined? SEPARATOR
|
5
|
+
DIRECTORY_PATH = '.device_directory' unless defined? DIRECTORY_PATH
|
6
|
+
DICTIONARY_PATH = 'dictionary.json' unless defined? DICTIONARY_PATH
|
7
|
+
INBOX_FILE_NAME = 'inbox.txt' unless defined? INBOX_FILE_NAME
|
8
|
+
FEATURES_PATH = './features' unless defined? FEATURES_PATH
|
9
|
+
DEFAULT_TIMEOUT_SECONDS = 30 unless defined? DEFAULT_TIMEOUT_SECONDS
|
10
|
+
ANDROID_PORTRAIT = 0 unless defined? ANDROID_PORTRAIT
|
11
|
+
ANDROID_LANDSCAPE = 1 unless defined? ANDROID_LANDSCAPE
|
12
|
+
WEB_PORTRAIT = 1 unless defined? WEB_PORTRAIT
|
13
|
+
MONKEY_DEFAULT_TIMEOUT = 5 unless defined? MONKEY_DEFAULT_TIMEOUT
|
14
|
+
WEB_DEVICE = 'WEB_DEVICE' unless defined? WEB_DEVICE
|
15
|
+
ANDROID_DEVICE = 'ANDROID_DEVICE' unless defined? ANDROID_DEVICE
|
16
|
+
PROPERTIES_PATH = 'PROPERTIES_PATH' unless defined? PROPERTIES_PATH
|
17
|
+
CONFIG_PATH = 'CONFIG_PATH' unless defined? CONFIG_PATH
|
18
|
+
REPORT_PATH = './reports' unless defined? REPORT_PATH
|
19
|
+
FILE_REPORT_NAME = 'report.json' unless defined? FILE_REPORT_NAME
|
20
|
+
D3_DATA_FILE_NAME = 'data.json' unless defined? D3_DATA_FILE_NAME
|
21
|
+
|
22
|
+
unless defined? DEVICES_REPORT_FILE_NAME
|
23
|
+
DEVICES_REPORT_FILE_NAME = 'devices.json'
|
24
|
+
end
|
25
|
+
|
26
|
+
unless defined? REPORT_ASSETS_PATH
|
27
|
+
REPORT_ASSETS_PATH = '../../../../reporter/assets/'
|
28
|
+
end
|
29
|
+
|
30
|
+
unless defined? DEFAULT_START_TIMEOUT_SECONDS
|
31
|
+
DEFAULT_START_TIMEOUT_SECONDS = 600 # 10.minutes
|
32
|
+
end
|
33
|
+
|
34
|
+
unless defined? DEFAULT_FINISH_TIMEOUT_SECONDS
|
35
|
+
DEFAULT_FINISH_TIMEOUT_SECONDS = 600 # 10.minutes
|
36
|
+
end
|
37
|
+
|
38
|
+
unless defined? DEVICES_READY_PATH
|
39
|
+
DEVICES_READY_PATH = '.devices_ready_to_start'
|
40
|
+
end
|
41
|
+
|
42
|
+
unless defined? DEVICES_FINISHED_PATH
|
43
|
+
DEVICES_FINISHED_PATH = '.devices_ready_to_finish'
|
44
|
+
end
|
45
|
+
|
46
|
+
unless defined? PROCESS_STATES
|
47
|
+
PROCESS_STATES = {
|
48
|
+
ready_to_start: 0,
|
49
|
+
ready_to_finish: 1
|
50
|
+
}.freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
unless defined? PROCESS_STATE_FILE_PATH
|
54
|
+
PROCESS_STATE_FILE_PATH = {
|
55
|
+
K::PROCESS_STATES[:ready_to_start] => DEVICES_READY_PATH,
|
56
|
+
K::PROCESS_STATES[:ready_to_finish] => DEVICES_FINISHED_PATH
|
57
|
+
}.freeze
|
58
|
+
end
|
59
|
+
|
60
|
+
unless defined? CALABASH_MONKEY_ACTIONS
|
61
|
+
CALABASH_MONKEY_ACTIONS = [
|
62
|
+
:move,
|
63
|
+
:down,
|
64
|
+
:up
|
65
|
+
].freeze
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,453 @@
|
|
1
|
+
require 'kraken-mobile/utils/k'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class Reporter
|
5
|
+
#-------------------------------
|
6
|
+
# Fields
|
7
|
+
#-------------------------------
|
8
|
+
PASSED = 'passed'.freeze
|
9
|
+
FAILED = 'failed'.freeze
|
10
|
+
SKIPPED = 'skipped'.freeze
|
11
|
+
PENDING = 'pending'.freeze
|
12
|
+
NOT_DEFINED = 'undefined'.freeze
|
13
|
+
AMBIGUOUS = 'ambiguous'.freeze
|
14
|
+
|
15
|
+
attr_accessor :test_scenario
|
16
|
+
|
17
|
+
#-------------------------------
|
18
|
+
# Constructors
|
19
|
+
#-------------------------------
|
20
|
+
def initialize(test_scenario:)
|
21
|
+
@test_scenario = test_scenario
|
22
|
+
end
|
23
|
+
|
24
|
+
#-------------------------------
|
25
|
+
# Lifecycle
|
26
|
+
#-------------------------------
|
27
|
+
def create_report_folder_requirements
|
28
|
+
create_report_execution_report_folder
|
29
|
+
create_devices_execution_report_folder
|
30
|
+
save_execution_devices_list
|
31
|
+
end
|
32
|
+
|
33
|
+
def save_report
|
34
|
+
generate_each_device_report
|
35
|
+
generate_general_report
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_general_report
|
39
|
+
erb_file = File.join(
|
40
|
+
File.expand_path('../../../../reporter', __FILE__),
|
41
|
+
'index.html.erb'
|
42
|
+
)
|
43
|
+
html_file = File.join(
|
44
|
+
File.expand_path("#{K::REPORT_PATH}/#{test_execution_id}/"),
|
45
|
+
'index.html'
|
46
|
+
)
|
47
|
+
report_file = open(
|
48
|
+
"#{K::REPORT_PATH}/#{test_execution_id}/#{K::DEVICES_REPORT_FILE_NAME}"
|
49
|
+
)
|
50
|
+
content = report_file.read
|
51
|
+
devices_report = report_by_devices
|
52
|
+
@features_report = fetures_from_report_by_devices(devices_report)
|
53
|
+
data_hash = feature_by_nodes_and_links @features_report
|
54
|
+
open(
|
55
|
+
"#{K::REPORT_PATH}/#{test_execution_id}/assets/js/#{K::D3_DATA_FILE_NAME}",
|
56
|
+
'w'
|
57
|
+
) do |file|
|
58
|
+
file.puts(data_hash.to_json)
|
59
|
+
end
|
60
|
+
template = File.read(erb_file)
|
61
|
+
result = ERB.new(template).result(binding)
|
62
|
+
File.open(html_file, 'w+') do |f|
|
63
|
+
f.write result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def generate_each_device_report
|
68
|
+
devices.each_with_index do |device, index|
|
69
|
+
generate_device_report(device, index + 1)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def generate_device_report(device, id)
|
74
|
+
process = MobileProcess.new(
|
75
|
+
id: id,
|
76
|
+
device: device,
|
77
|
+
test_scenario: @test_scenario
|
78
|
+
)
|
79
|
+
@apk_path = process.apk_path
|
80
|
+
report_file = open("#{K::REPORT_PATH}/#{test_execution_id}/#{device.id}/#{K::FILE_REPORT_NAME}")
|
81
|
+
content = report_file.read
|
82
|
+
@features = JSON.parse(content)
|
83
|
+
@total_scenarios = total_scenarios @features
|
84
|
+
@device = device
|
85
|
+
@total_failed_scenarios_percentage = total_failed_scenarios_percentage @features
|
86
|
+
@total_passed_scenarios_percentage = total_passed_scenarios_percentage @features
|
87
|
+
@total_passed_features_percentage = total_passed_features_percentage @features
|
88
|
+
@total_failed_features_percentage = total_failed_features_percentage @features
|
89
|
+
erb_file = File.join(File.expand_path('../../../../reporter', __FILE__), "feature_report.html.erb")
|
90
|
+
html_file = File.join(File.expand_path("#{K::REPORT_PATH}/#{test_execution_id}/#{device.id}/"), File.basename(erb_file, '.erb')) #=>"page.html"
|
91
|
+
# Variables
|
92
|
+
template = File.read(erb_file)
|
93
|
+
result = ERB.new(template).result(binding)
|
94
|
+
# write result to file
|
95
|
+
File.open(html_file, 'w+') do |f|
|
96
|
+
f.write result
|
97
|
+
end
|
98
|
+
generate_features_report @features, device
|
99
|
+
end
|
100
|
+
|
101
|
+
#-------------------------------
|
102
|
+
# Methods
|
103
|
+
#-------------------------------
|
104
|
+
def test_execution_id
|
105
|
+
raise 'ERROR: Invalid test scenario' if @test_scenario.nil?
|
106
|
+
|
107
|
+
@test_scenario.execution_id
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_report_execution_report_folder
|
111
|
+
Dir.mkdir(K::REPORT_PATH) unless File.exist?(K::REPORT_PATH)
|
112
|
+
Dir.mkdir("#{K::REPORT_PATH}/#{test_execution_id}")
|
113
|
+
FileUtils.cp_r(
|
114
|
+
File.expand_path(K::REPORT_ASSETS_PATH, __FILE__),
|
115
|
+
"#{K::REPORT_PATH}/#{test_execution_id}/"
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def save_execution_devices_list
|
120
|
+
open(
|
121
|
+
"#{K::REPORT_PATH}/#{test_execution_id}/#{K::DEVICES_REPORT_FILE_NAME}",
|
122
|
+
'w'
|
123
|
+
) do |file|
|
124
|
+
file.puts(devices_json.to_json)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def create_devices_execution_report_folder
|
129
|
+
devices.each do |device|
|
130
|
+
Dir.mkdir(
|
131
|
+
"#{KrakenMobile::Constants::REPORT_PATH}/#{test_execution_id}/"\
|
132
|
+
"#{device.id}"
|
133
|
+
)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def generate_features_report features, device
|
140
|
+
features.each do |feature|
|
141
|
+
generate_feature_report feature, device
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def generate_feature_report feature, device
|
146
|
+
Dir.mkdir("#{K::REPORT_PATH}/#{test_execution_id}/#{device.id}/features_report") unless File.exists?("#{K::REPORT_PATH}/#{test_execution_id}/#{device.id}/features_report")
|
147
|
+
file_name = feature_id feature
|
148
|
+
erb_file = File.join(File.expand_path('../../../../reporter', __FILE__), "scenario_report.html.erb")
|
149
|
+
html_file = File.join(File.expand_path("#{K::REPORT_PATH}/#{test_execution_id}/#{device.id}/features_report"), "#{file_name}.html") #=>"page.html"
|
150
|
+
# Variables
|
151
|
+
@feature = feature
|
152
|
+
template = File.read(erb_file)
|
153
|
+
result = ERB.new(template).result(binding)
|
154
|
+
# write result to file
|
155
|
+
File.open(html_file, 'w+') do |f|
|
156
|
+
f.write result
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def report_by_devices
|
161
|
+
devices_report = {}
|
162
|
+
devices_json.each do |device|
|
163
|
+
unless File.exist?(
|
164
|
+
"#{K::REPORT_PATH}/#{test_execution_id}/#{device[:id]}/#{K::FILE_REPORT_NAME}"
|
165
|
+
)
|
166
|
+
next
|
167
|
+
end
|
168
|
+
|
169
|
+
report_file = open(
|
170
|
+
"#{K::REPORT_PATH}/#{test_execution_id}/#{device[:id]}/#{K::FILE_REPORT_NAME}"
|
171
|
+
)
|
172
|
+
content = report_file.read
|
173
|
+
devices_report[device[:user]] = JSON.parse(content)
|
174
|
+
devices_report[device[:user]].each do |d| d['device_model'] = device[:model] if !d['device_model'] end
|
175
|
+
devices_report[device[:user]].each do |d| d['device_id'] = device[:id] if !d['device_id'] end
|
176
|
+
end
|
177
|
+
devices_report
|
178
|
+
end
|
179
|
+
|
180
|
+
def fetures_from_report_by_devices report_by_devices
|
181
|
+
features = {}
|
182
|
+
report_by_devices.keys.each do |user_key|
|
183
|
+
report = report_by_devices[user_key]
|
184
|
+
report.each do |feature|
|
185
|
+
features[feature["id"]] = {} if !features[feature["id"]]
|
186
|
+
features[feature["id"]]["name"] = feature["name"] if !features[feature["id"]]["name"] && feature["name"]
|
187
|
+
features[feature["id"]]["devices"] = {} if !features[feature["id"]]["devices"]
|
188
|
+
if feature["elements"] && feature["elements"].count > 0
|
189
|
+
features[feature["id"]]["devices"][user_key] = []
|
190
|
+
if feature["elements"].first["steps"]
|
191
|
+
failed = false
|
192
|
+
feature["elements"].first["steps"].each do |step|
|
193
|
+
next if failed
|
194
|
+
failed = step["result"]["status"] != PASSED
|
195
|
+
image = nil
|
196
|
+
image = step["after"].first["embeddings"].first["data"] if step["after"] && step["after"].count > 0 && step["after"].first["embeddings"] && step["after"].first["embeddings"].count > 0
|
197
|
+
features[feature["id"]]["devices"][user_key] << {
|
198
|
+
name: "#{step['keyword']} #{step['name']}",
|
199
|
+
duration: step["result"]["duration"],
|
200
|
+
image: image,
|
201
|
+
device_model: feature["device_model"],
|
202
|
+
status: failed ? FAILED : PASSED
|
203
|
+
}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
features
|
210
|
+
end
|
211
|
+
|
212
|
+
def feature_by_nodes_and_links features_report
|
213
|
+
features = []
|
214
|
+
features_report.values.each do |feature|
|
215
|
+
features << nodes_and_links(feature["devices"], feature["name"]) if feature["devices"]
|
216
|
+
end
|
217
|
+
features
|
218
|
+
end
|
219
|
+
|
220
|
+
def nodes_and_links feature_report, feature_name
|
221
|
+
last_node_id = 0
|
222
|
+
nodes = [{ name: "", id: "empty", image: nil }]
|
223
|
+
signal_hash = {}
|
224
|
+
links = []
|
225
|
+
feature_report.keys.each do |key|
|
226
|
+
steps = feature_report[key]
|
227
|
+
coming_from_signal = false
|
228
|
+
last_signal = -1
|
229
|
+
steps.each_with_index do |step, index|
|
230
|
+
node_id = last_node_id+1
|
231
|
+
if isReadSignal(step[:name]) && step[:status] == PASSED
|
232
|
+
signal = signalContent(step[:name])
|
233
|
+
already_created_signal = signal_hash[signal] ? true : false
|
234
|
+
signal_hash[signal] = already_created_signal ? signal_hash[signal] : { id: "#{node_id}", receiver: key }
|
235
|
+
node = { name: "Signal: #{signal}, Receiver: #{step[:device_model]}", id: signal_hash[signal][:id], image: nil, status: step[:status] }
|
236
|
+
if already_created_signal
|
237
|
+
entry = nodes.select{ |node| node[:id] == signal_hash[signal][:id] }.first
|
238
|
+
entry[:name] = "Signal: #{signal}, Receiver: #{step[:device_model]}" if entry
|
239
|
+
end
|
240
|
+
source = (coming_from_signal ? last_signal : (index == 0 ? 0 : last_node_id))
|
241
|
+
link = {
|
242
|
+
source: source,
|
243
|
+
target: signal_hash[signal][:id].to_i,
|
244
|
+
value: 1,
|
245
|
+
owner: key,
|
246
|
+
owner_model: step[:device_model]
|
247
|
+
}
|
248
|
+
nodes << node if !already_created_signal
|
249
|
+
links << link
|
250
|
+
last_node_id += 1 if !already_created_signal
|
251
|
+
last_signal = signal_hash[signal][:id].to_i
|
252
|
+
coming_from_signal = true
|
253
|
+
elsif isWriteSignal(step[:name]) && step[:status] == PASSED
|
254
|
+
signal = signalContent(step[:name])
|
255
|
+
receiver = signalReceiver(step[:name])
|
256
|
+
already_created_signal = signal_hash[signal] ? true : false
|
257
|
+
signal_hash[signal] = already_created_signal ? signal_hash[signal] : { id: "#{node_id}", receiver: receiver }
|
258
|
+
node = { name: step[:name], id: signal_hash[signal][:id], image: nil, status: step[:status] }
|
259
|
+
source = (coming_from_signal ? last_signal : (index == 0 ? 0 : last_node_id))
|
260
|
+
link = {
|
261
|
+
source: source,
|
262
|
+
target: signal_hash[signal][:id].to_i,
|
263
|
+
value: 1,
|
264
|
+
owner: key,
|
265
|
+
owner_model: step[:device_model]
|
266
|
+
}
|
267
|
+
nodes << node if !already_created_signal
|
268
|
+
links << link
|
269
|
+
last_node_id += 1 if !already_created_signal
|
270
|
+
last_signal = signal_hash[signal][:id].to_i
|
271
|
+
coming_from_signal = true
|
272
|
+
else
|
273
|
+
node = { name: step[:name], id: "#{node_id}", image: step[:image], status: step[:status] }
|
274
|
+
source = (coming_from_signal ? last_signal : (index == 0 ? 0 : last_node_id))
|
275
|
+
link = {
|
276
|
+
source: source,
|
277
|
+
target: node_id,
|
278
|
+
value: 1,
|
279
|
+
owner: key,
|
280
|
+
owner_model: step[:device_model]
|
281
|
+
}
|
282
|
+
nodes << node
|
283
|
+
links << link
|
284
|
+
last_node_id += 1
|
285
|
+
coming_from_signal = false
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
return {
|
290
|
+
name: feature_name,
|
291
|
+
nodes: nodes,
|
292
|
+
links: links
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
296
|
+
def isReadSignal step
|
297
|
+
line = step.split(' ')[1..-1].join(' ')
|
298
|
+
(line =~ /^I wait for a signal containing "([^\"]*)"$/ ? true : false) || (line =~ /^I wait for a signal containing "([^\"]*)" for (\d+) seconds$/ ? true : false)
|
299
|
+
end
|
300
|
+
|
301
|
+
def isWriteSignal step
|
302
|
+
line = step.split(' ')[1..-1].join(' ')
|
303
|
+
line =~ /^I send a signal to user (\d+) containing "([^\"]*)"$/ ? true : false
|
304
|
+
end
|
305
|
+
|
306
|
+
def signalContent step
|
307
|
+
line = step.split(' ')[1..-1].join(' ')
|
308
|
+
line.scan(/"([^\"]*)"/).first.first if line.scan(/"([^\"]*)"/).first
|
309
|
+
end
|
310
|
+
|
311
|
+
def signalReceiver step
|
312
|
+
line = step.split(' ')[1..-1].join(' ')
|
313
|
+
line.scan(/(\d+)/).first.first if line.scan(/(\d+)/).first
|
314
|
+
end
|
315
|
+
|
316
|
+
def devices
|
317
|
+
raise 'ERROR: Invalid test scenario' if @test_scenario.nil?
|
318
|
+
|
319
|
+
@test_scenario.devices
|
320
|
+
end
|
321
|
+
|
322
|
+
def devices_json
|
323
|
+
devices.map.with_index do |device, index|
|
324
|
+
height, width = device.screen_size
|
325
|
+
{
|
326
|
+
user: (index + 1), id: device.id,
|
327
|
+
model: device.model, screen_height: height,
|
328
|
+
screen_width: width, sdk: device.sdk_version,
|
329
|
+
type: device.type
|
330
|
+
}
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def total_scenarios features
|
335
|
+
how_many = 0
|
336
|
+
features.each do |feature|
|
337
|
+
scenarios = feature["elements"]
|
338
|
+
how_many += scenarios.count if scenarios
|
339
|
+
end
|
340
|
+
how_many
|
341
|
+
end
|
342
|
+
|
343
|
+
def failed_scenarios feature
|
344
|
+
scenarios = feature["elements"]
|
345
|
+
scenarios.select{ |scenario|
|
346
|
+
steps = scenario["steps"]
|
347
|
+
steps.any?{ |step| step["result"] && step["result"]["status"] != PASSED }
|
348
|
+
}
|
349
|
+
end
|
350
|
+
|
351
|
+
def total_passed_scenarios features
|
352
|
+
how_many = 0
|
353
|
+
features.each do |feature|
|
354
|
+
how_many += passed_scenarios(feature).count
|
355
|
+
end
|
356
|
+
how_many
|
357
|
+
end
|
358
|
+
|
359
|
+
def total_failed_scenarios features
|
360
|
+
how_many = 0
|
361
|
+
features.each do |feature|
|
362
|
+
how_many += failed_scenarios(feature).count
|
363
|
+
end
|
364
|
+
how_many
|
365
|
+
end
|
366
|
+
|
367
|
+
def total_passed_features features
|
368
|
+
how_many = 0
|
369
|
+
features.each do |feature|
|
370
|
+
how_many += 1 if feature_passed?(feature)
|
371
|
+
end
|
372
|
+
how_many
|
373
|
+
end
|
374
|
+
|
375
|
+
def total_failed_features features
|
376
|
+
how_many = 0
|
377
|
+
features.each do |feature|
|
378
|
+
how_many += 1 if !feature_passed?(feature)
|
379
|
+
end
|
380
|
+
how_many
|
381
|
+
end
|
382
|
+
|
383
|
+
def feature_passed_scenarios_percentage feature
|
384
|
+
(passed_scenarios(feature).count.to_f/feature["elements"].count.to_f).round(2) * 100.00
|
385
|
+
end
|
386
|
+
|
387
|
+
def feature_failed_scenarios_percentage feature
|
388
|
+
(failed_scenarios(feature).count.to_f/feature["elements"].count.to_f).round(2) * 100.00
|
389
|
+
end
|
390
|
+
|
391
|
+
def total_passed_scenarios_percentage features
|
392
|
+
(total_passed_scenarios(features).to_f/total_scenarios(features).to_f).round(2) * 100.00
|
393
|
+
end
|
394
|
+
|
395
|
+
def total_passed_features_percentage features
|
396
|
+
(total_passed_features(features).to_f/features.count.to_f).round(2) * 100.00
|
397
|
+
end
|
398
|
+
|
399
|
+
def total_failed_scenarios_percentage features
|
400
|
+
(total_failed_scenarios(features).to_f/total_scenarios(features).to_f).round(2) * 100.00
|
401
|
+
end
|
402
|
+
|
403
|
+
def total_failed_features_percentage features
|
404
|
+
(total_failed_features(features).to_f/features.count.to_f).round(2) * 100.00
|
405
|
+
end
|
406
|
+
|
407
|
+
def feature_passed? feature
|
408
|
+
passed_scenarios(feature).count == feature["elements"].count
|
409
|
+
end
|
410
|
+
|
411
|
+
def format_duration(nanoseconds)
|
412
|
+
duration_in_seconds = nanoseconds.to_f/1000000000.0
|
413
|
+
m, s = duration_in_seconds.divmod(60)
|
414
|
+
"#{m}m #{format('%.3f', s)}s"
|
415
|
+
end
|
416
|
+
|
417
|
+
def passed_features features
|
418
|
+
features.select{ |feature| passed_scenarios(feature) == feature["elements"].count }
|
419
|
+
end
|
420
|
+
|
421
|
+
def failed_features features
|
422
|
+
features.select{ |feature| failed_scenarios(feature) == feature["elements"].count }
|
423
|
+
end
|
424
|
+
|
425
|
+
def feature_duration feature
|
426
|
+
scenarios = feature["elements"]
|
427
|
+
how_long = 0
|
428
|
+
scenarios.each do |scenario|
|
429
|
+
how_long += scenario_duration(scenario)
|
430
|
+
end
|
431
|
+
how_long
|
432
|
+
end
|
433
|
+
|
434
|
+
def scenario_duration scenario
|
435
|
+
how_long = 0
|
436
|
+
scenario["steps"].each do |step|
|
437
|
+
how_long += step["result"]["duration"] if step["result"] && step["result"]["duration"]
|
438
|
+
end
|
439
|
+
how_long
|
440
|
+
end
|
441
|
+
|
442
|
+
def passed_scenarios feature
|
443
|
+
scenarios = feature["elements"]
|
444
|
+
scenarios.select{ |scenario|
|
445
|
+
steps = scenario["steps"]
|
446
|
+
steps.all?{ |step| step["result"] && step["result"]["status"] == PASSED }
|
447
|
+
}
|
448
|
+
end
|
449
|
+
|
450
|
+
def feature_id feature
|
451
|
+
Digest::SHA256.hexdigest("#{feature["id"].strip}#{feature["uri"].strip}")
|
452
|
+
end
|
453
|
+
end
|