kraken-mobile 1.0.4 → 1.0.5
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 +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
|