kraken-mobile 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +8 -0
  3. data/README.md +169 -0
  4. data/bin/kraken-mobile +91 -0
  5. data/bin/kraken-mobile-calabash-android.rb +85 -0
  6. data/bin/kraken-mobile-generate.rb +19 -0
  7. data/bin/kraken-mobile-helpers.rb +48 -0
  8. data/bin/kraken-mobile-setup.rb +50 -0
  9. data/calabash-android-features-skeleton/my_first.feature +11 -0
  10. data/calabash-android-features-skeleton/step_definitions/kraken_steps.rb +1 -0
  11. data/calabash-android-features-skeleton/support/app_installation_hooks.rb +25 -0
  12. data/calabash-android-features-skeleton/support/app_life_cycle_hooks.rb +10 -0
  13. data/calabash-android-features-skeleton/support/env.rb +1 -0
  14. data/lib/kraken-mobile.rb +29 -0
  15. data/lib/kraken-mobile/constants.rb +25 -0
  16. data/lib/kraken-mobile/helpers/command_helper.rb +38 -0
  17. data/lib/kraken-mobile/helpers/devices_helper/adb_helper.rb +157 -0
  18. data/lib/kraken-mobile/helpers/devices_helper/manager.rb +43 -0
  19. data/lib/kraken-mobile/helpers/feature_analyzer.rb +28 -0
  20. data/lib/kraken-mobile/helpers/feature_grouper.rb +48 -0
  21. data/lib/kraken-mobile/helpers/reporter.rb +381 -0
  22. data/lib/kraken-mobile/models/device.rb +32 -0
  23. data/lib/kraken-mobile/protocols/file_protocol.rb +126 -0
  24. data/lib/kraken-mobile/runners/calabash/android/android_runner.rb +130 -0
  25. data/lib/kraken-mobile/runners/calabash/android/apk_signer.rb +14 -0
  26. data/lib/kraken-mobile/runners/calabash/android/cucumber.rb +27 -0
  27. data/lib/kraken-mobile/runners/calabash/android/kraken_hooks.rb +15 -0
  28. data/lib/kraken-mobile/runners/calabash/android/kraken_steps.rb +3 -0
  29. data/lib/kraken-mobile/runners/calabash/android/monkey_helper.rb +139 -0
  30. data/lib/kraken-mobile/runners/calabash/android/operations.rb +44 -0
  31. data/lib/kraken-mobile/runners/calabash/android/steps/communication_steps.rb +57 -0
  32. data/lib/kraken-mobile/runners/calabash/monkey/monkey_runner.rb +113 -0
  33. data/lib/kraken-mobile/runners/runner.rb +18 -0
  34. data/lib/kraken-mobile/version.rb +5 -0
  35. data/reporter/assets/css/bootstrap.min.css +6 -0
  36. data/reporter/assets/css/dataTables.bootstrap.min.css +1 -0
  37. data/reporter/assets/css/feature_index.css +449 -0
  38. data/reporter/assets/css/font-awesome.min.css +4 -0
  39. data/reporter/assets/css/responsive.dataTables.min.css +1 -0
  40. data/reporter/assets/css/scenario_index.css +461 -0
  41. data/reporter/assets/fonts/FontAwesome.otf +0 -0
  42. data/reporter/assets/fonts/fontawesome-webfont.eot +0 -0
  43. data/reporter/assets/fonts/fontawesome-webfont.svg +2671 -0
  44. data/reporter/assets/fonts/fontawesome-webfont.ttf +0 -0
  45. data/reporter/assets/fonts/fontawesome-webfont.woff +0 -0
  46. data/reporter/assets/fonts/fontawesome-webfont.woff2 +0 -0
  47. data/reporter/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  48. data/reporter/assets/fonts/glyphicons-halflings-regular.svg +288 -0
  49. data/reporter/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  50. data/reporter/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  51. data/reporter/assets/fonts/glyphicons-halflings-regular.woff2 +0 -0
  52. data/reporter/assets/images/kraken.png +0 -0
  53. data/reporter/assets/js/Chart.min.js +14 -0
  54. data/reporter/assets/js/bootstrap.min.js +7 -0
  55. data/reporter/assets/js/d3.chart.min.js +7 -0
  56. data/reporter/assets/js/d3.v3.min.js +5 -0
  57. data/reporter/assets/js/dataTables.bootstrap.min.js +8 -0
  58. data/reporter/assets/js/dataTables.responsive.min.js +26 -0
  59. data/reporter/assets/js/first-sankey.js +292 -0
  60. data/reporter/assets/js/gitgraph.min.js +10 -0
  61. data/reporter/assets/js/html5shiv.min.js +4 -0
  62. data/reporter/assets/js/jquery-3.2.1.min.js +4 -0
  63. data/reporter/assets/js/jquery.dataTables.min.js +167 -0
  64. data/reporter/assets/js/respond.min.js +5 -0
  65. data/reporter/assets/js/sankey.js +512 -0
  66. data/reporter/feature_report.html.erb +274 -0
  67. data/reporter/index.html.erb +711 -0
  68. data/reporter/scenario_report.html.erb +174 -0
  69. 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,10 @@
1
+ require 'kraken-mobile/runners/calabash/android/Operations'
2
+
3
+ Before do |scenario|
4
+ start_kraken_test_server_in_background scenario
5
+ end
6
+
7
+ After do |scenario|
8
+ shutdown_kraken_test_server scenario
9
+ uninstall_app_with_calabash
10
+ end
@@ -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