kraken-mobile 1.0.0

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.
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