parallel_report_portal 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ module ParallelReportPortal
2
+ # This module is responsilbe for the timekeeping for the tests.
3
+ module Clock
4
+ # Get the current time.
5
+ #
6
+ # This is based on the Unix time stamp and is in milliseconds.
7
+ #
8
+ # @return [Integer] the number of milliseconds since the Unix epoc.
9
+ def clock
10
+ (Time.now.to_f * 1000).to_i
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,138 @@
1
+ module ParallelReportPortal
2
+ # The Configuration class holds the connection properties to communicate with
3
+ # Report Portal and to identify the user and project for reporting.
4
+ #
5
+ # It attempts to load a configuration file called +report_portal.yml+ first in a
6
+ # local directory called +config+ and if that's not found in the current directory.
7
+ # (Report Portal actually tells you to create a files called +REPORT_PORTAL.YML+ in
8
+ # uppercase -- for this reason the initializer is case insensitive with regards to
9
+ # the file name)
10
+ #
11
+ # It will then try an apply the following environment variables, if present (these
12
+ # can be specified in either lowercase for backwards compatibility with the official
13
+ # gem or in uppercase for reasons of sanity)
14
+ #
15
+ # == Environment variables
16
+ #
17
+ # RP_UUID:: The UUID of the user associated with this launch
18
+ # RP_ENDPOINT:: the URL of the Report Portal API endpoint
19
+ # RP_PROJECT:: the Report Portal project name -- this must already exist within Report Port and this user must be a member of the project
20
+ # RP_LAUNCH:: The name of this launch
21
+ # RP_DESCRIPTION:: A textual string describing this launch
22
+ # RP_TAGS:: A set of tags to pass to Report Portal for this launch. If these are set via an environment variable, provide a comma-separated string of tags
23
+ # RP_ATTRIBUTES:: A set of attribute tags to pass to Report Portal for this launch. If these are set via an environment variable, provide a comma-separated string of attributes
24
+ class Configuration
25
+ ATTRIBUTES = [:uuid, :endpoint, :project, :launch, :debug, :description, :tags, :attributes]
26
+
27
+ # @return [String] the Report Portal user UUID
28
+ attr_accessor :uuid
29
+ # @return [String] the Report Portal URI - this should include the scheme
30
+ # e.g. +https://reportportal.local/api/v1+
31
+ attr_accessor :endpoint
32
+ # @return [String] the Report Portal project name.
33
+ # This must exist and match the project name within
34
+ # Report Portal.
35
+ attr_accessor :project
36
+ # @return [String] the launch name for this test run.
37
+ attr_accessor :launch
38
+ # @return [Array<String>] an array of tags to attach to this launch.
39
+ attr_reader :tags
40
+ # @return [Boolean] true if this is a debug run (this launch will appear
41
+ # on the debug tab in Report Portal).
42
+ attr_reader :debug
43
+ # @return [String] a textual description of this launch.
44
+ attr_accessor :description
45
+ # @return [Array<String>] an array of attributes to attach to this launch
46
+ # (Report Portal 5)
47
+ attr_reader :attributes
48
+
49
+
50
+ # Create an instance of Configuration.
51
+ #
52
+ # The initializer will first attempt to load a configuration files called
53
+ # +report_portal.yml+ (case insensitive) in the both the +config+ and current
54
+ # working directory (the former takes precidence). It will then apply
55
+ # any of the environment variable values.
56
+ def initialize
57
+ load_configuration_file
58
+ ATTRIBUTES.each do |attr|
59
+ env_value = get_env("rp_#{attr.to_s}")
60
+ send(:"#{attr}=", env_value) if env_value
61
+ end
62
+ end
63
+
64
+ # Sets the tags for the launch. If an array is provided, the array is used,
65
+ # if a string is provided, the string is broken into components by splitting
66
+ # on a comma.
67
+ #
68
+ # e.g.
69
+ # configuration.tags="one,two, three"
70
+ # #=> ["one", "two", "three"]
71
+ #
72
+ # param [String | Array<String>] taglist a list of tags to set
73
+ def tags=(taglist)
74
+ if taglist.is_a?(String)
75
+ @tags = taglist.split(',').map(&:strip)
76
+ elsif taglist.is_a?(Array)
77
+ @tags = taglist
78
+ else
79
+ @tags = []
80
+ end
81
+ tags
82
+ end
83
+
84
+
85
+ # Enables the debug flag which is sent to Report Portal. If this flag is set
86
+ # Report Portal will include this launch in its 'debug' tab.
87
+ #
88
+ # param [Boolean | String] value if the value is a Boolean, it will take that value
89
+ # if it is a String, it will set values of 'true' to +true+, else all values will be false.
90
+ def debug=(value)
91
+ @debug = if [true, false].include?(value)
92
+ value
93
+ else
94
+ value.to_s.downcase.strip == 'true'
95
+ end
96
+ end
97
+
98
+ # Sets the attributes for the launch. If an array is provided, the array is used,
99
+ # if a string is provided, the string is broken into components by splitting
100
+ # on a comma.
101
+ #
102
+ # e.g.
103
+ # configuration.tags="one,two, three"
104
+ # #=> ["one", "two", "three"]
105
+ #
106
+ # param [String | Array<String>] taglist a list of tags to set
107
+ def attributes=(attrlist)
108
+ if attrlist.is_a?(String)
109
+ @attributes = attrlist.split(',').map(&:strip)
110
+ elsif attrlist.is_a?(Array)
111
+ @attributes = attrlist
112
+ else
113
+ @attributes = []
114
+ end
115
+ attributes
116
+ end
117
+
118
+ private
119
+
120
+ def get_env(name)
121
+ ENV[name.upcase] || ENV[name.downcase]
122
+ end
123
+
124
+ def load_configuration_file
125
+ files = Dir['./config/*'] + Dir['./*']
126
+ files
127
+ .filter { |fn| fn.downcase.end_with?('/report_portal.yml') }
128
+ .first
129
+ .then { |fn| fn ? File.read(fn) : '' }
130
+ .then { |ys| YAML.safe_load(ys, symbolize_names: true) }
131
+ .then do |yaml|
132
+ ATTRIBUTES.each do |attr|
133
+ send(:"#{attr}=", yaml[attr]) if yaml&.fetch(attr, nil)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'report'
2
+
3
+ module ParallelReportPortal
4
+ module Cucumber
5
+ # Formatter supporting the Cucumber formatter API.
6
+ # This is the class which does the heavy-lifting by
7
+ # integrating with cucumber.
8
+ class Formatter
9
+
10
+ CucumberMessagesVersion=[4,0,0]
11
+
12
+ # Create a new formatter instance
13
+ #
14
+ # @param [Cucumber::Configuration] cucumber_config the cucumber configuration environment
15
+ def initialize(cucumber_config)
16
+ @ast_lookup = if (::Cucumber::VERSION.split('.').map(&:to_i) <=> CucumberMessagesVersion) > 0
17
+ require 'cucumber/formatter/ast_lookup'
18
+ ::Cucumber::Formatter::AstLookup.new(cucumber_config)
19
+ else
20
+ nil
21
+ end
22
+ start_background_thread.priority = Thread.main.priority + 1
23
+ register_event_handlers(cucumber_config)
24
+ end
25
+
26
+ private
27
+
28
+ def report
29
+ @report ||= Report.new(@ast_lookup)
30
+ end
31
+
32
+ def register_event_handlers(config)
33
+ [:test_case_started,
34
+ :test_case_finished,
35
+ :test_step_started,
36
+ :test_step_finished].each do |event_name|
37
+ config.on_event(event_name) do |event|
38
+ background_queue << -> { report.public_send(event_name, event, ParallelReportPortal.clock) }
39
+ end
40
+ end
41
+ config.on_event :test_run_started, &method(:handle_test_run_started )
42
+ config.on_event :test_run_finished, &method(:handle_test_run_finished)
43
+ end
44
+
45
+ def handle_test_run_started(event)
46
+ background_queue << proc { report.launch_started(ParallelReportPortal.clock) }
47
+ end
48
+
49
+ def background_queue
50
+ @background_queue ||= Queue.new
51
+ end
52
+
53
+ def start_background_thread
54
+ @background_thread ||= Thread.new do
55
+ loop do
56
+ code = background_queue.shift
57
+ code.call
58
+ end
59
+ end
60
+ end
61
+
62
+ def handle_test_run_finished(event)
63
+ background_queue << proc do
64
+ report.feature_finished(ParallelReportPortal.clock)
65
+ report.launch_finished(ParallelReportPortal.clock)
66
+ end
67
+ sleep 0.01 while !background_queue.empty? || background_queue.num_waiting == 0
68
+ @background_thread.kill
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,226 @@
1
+ require 'faraday'
2
+ require 'tree'
3
+
4
+ module ParallelReportPortal
5
+ module Cucumber
6
+ # Report object. This handles the management of the state heirarchy and
7
+ # the issuing of the requests to the HTTP module.
8
+ class Report
9
+
10
+ attr_reader :launch_id
11
+
12
+ Feature = Struct.new(:feature, :id)
13
+
14
+ LOG_LEVELS = {
15
+ error: 'ERROR',
16
+ warn: 'WARN',
17
+ info: 'INFO',
18
+ debug: 'DEBUG',
19
+ trace: 'TRACE',
20
+ fatal: 'FATAL',
21
+ unknown: 'UNKNOWN'
22
+ }
23
+
24
+
25
+ # Create a new instance of the report
26
+ def initialize(ast_lookup = nil)
27
+ @feature = nil
28
+ @tree = Tree::TreeNode.new( 'root' )
29
+ @ast_lookup = ast_lookup
30
+ end
31
+
32
+ # Issued to start a launch. It is possilbe that this method could be called
33
+ # from multiple processes for the same launch if this is being run with
34
+ # parallel tests enabled. A temporary launch file will be created (using
35
+ # exclusive locking). The first time this method is called it will write the
36
+ # launch id to the launch file, subsequent calls by other processes will read
37
+ # this launch id and use that.
38
+ #
39
+ # @param start_time [Integer] the millis from the epoch
40
+ # @return [String] the UUID of this launch
41
+ def launch_started(start_time)
42
+ ParallelReportPortal.file_open_exlock_and_block(ParallelReportPortal.launch_id_file, 'a+' ) do |file|
43
+ if file.size == 0
44
+ @launch_id = ParallelReportPortal.req_launch_started(start_time)
45
+ file.write(@launch_id)
46
+ file.flush
47
+ else
48
+ @launch_id = file.readline
49
+ end
50
+ @launch_id
51
+ end
52
+ end
53
+
54
+ # Called to finish a launch. Any open children items will be closed in the process.
55
+ #
56
+ # @param clock [Integer] the millis from the epoch
57
+ def launch_finished(clock)
58
+ @tree.postordered_each do |node|
59
+ ParallelReportPortal.req_feature_finished(node.content, clock) unless node.is_root?
60
+ end
61
+ ParallelReportPortal.req_launch_finished(launch_id, clock)
62
+ end
63
+
64
+ # Called to indicate that a feature has started.
65
+ #
66
+ # @param
67
+ def feature_started(feature, clock)
68
+ parent_id = hierarchy(feature, clock)
69
+ feature = feature.feature if using_cucumber_messages?
70
+ ParallelReportPortal.req_feature_started(launch_id, parent_id, feature, clock)
71
+ end
72
+
73
+ def feature_finished(clock)
74
+ if @feature
75
+ resp = ParallelReportPortal.req_feature_finished(@feature.id, clock)
76
+ end
77
+ end
78
+
79
+ def test_case_started(event, clock)
80
+ test_case = lookup_test_case(event.test_case)
81
+ feature = lookup_feature(event.test_case)
82
+ feature = current_feature(feature, clock)
83
+ @test_case_id = ParallelReportPortal.req_test_case_started(launch_id, feature.id, test_case, clock)
84
+ end
85
+
86
+ def test_case_finished(event, clock)
87
+ result = event.result
88
+ status = result.to_sym
89
+ failure_message = nil
90
+ if [:undefined, :pending].include?(status)
91
+ status = :failed
92
+ failure_message = result.message
93
+ end
94
+ resp = ParallelReportPortal.req_test_case_finished(@test_case_id, status, clock)
95
+ end
96
+
97
+ def test_step_started(event, clock)
98
+ test_step = event.test_step
99
+ if !hook?(test_step)
100
+ step_source = lookup_step_source(test_step)
101
+ detail = "#{step_source.keyword} #{step_source.text}"
102
+ if (using_cucumber_messages? ? test_step : step_source).multiline_arg.doc_string?
103
+ detail << %(\n"""\n#{(using_cucumber_messages? ? test_step : step_source).multiline_arg.content}\n""")
104
+ elsif (using_cucumber_messages? ? test_step : step_source).multiline_arg.data_table?
105
+ detail << (using_cucumber_messages? ? test_step : step_source).multiline_arg.raw.reduce("\n") {|acc, row| acc << "| #{row.join(' | ')} |\n"}
106
+ end
107
+
108
+ ParallelReportPortal.req_log(@test_case_id, detail, status_to_level(:trace), clock)
109
+ end
110
+ end
111
+
112
+ def test_step_finished(event, clock)
113
+ test_step = event.test_step
114
+ result = event.result
115
+ status = result.to_sym
116
+ if !hook?(test_step)
117
+ step_source = lookup_step_source(test_step)
118
+ detail = "#{step_source.text}"
119
+
120
+ if [:failed, :pending, :undefined].include?(status)
121
+ level = :error
122
+ detail = if [:failed, :pending].include?(status)
123
+ ex = result.exception
124
+ sprintf("%s: %s\n %s", ex.class.name, ex.message, ex.backtrace.join("\n "))
125
+ else
126
+ sprintf("Undefined step: %s:\n%s", test_step.text, test_step.source.last.backtrace_line)
127
+ end
128
+
129
+ ParallelReportPortal.req_log(@test_case_id, detail, level, clock)
130
+ end
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def using_cucumber_messages?
137
+ @ast_lookup != nil
138
+ end
139
+
140
+ def hierarchy(feature, clock)
141
+ node = nil
142
+ path_components = if using_cucumber_messages?
143
+ feature.uri.split(File::SEPARATOR)
144
+ else
145
+ feature.location.file.split(File::SEPARATOR)
146
+ end
147
+ ParallelReportPortal.file_open_exlock_and_block(ParallelReportPortal.hierarchy_file, 'a+b' ) do |file|
148
+ @tree = Marshal.load(File.read(file)) if file.size > 0
149
+ node = @tree.root
150
+ path_components[0..-2].each do |component|
151
+ next_node = node[component]
152
+ unless next_node
153
+ id = ParallelReportPortal.req_hierarchy(launch_id, "Folder: #{component}", node.content, 'SUITE', [], nil, clock )
154
+ next_node = Tree::TreeNode.new(component, id)
155
+ node << next_node
156
+ node = next_node
157
+ else
158
+ node = next_node
159
+ end
160
+ end
161
+ file.truncate(0)
162
+ file.write(Marshal.dump(@tree))
163
+ file.flush
164
+ end
165
+
166
+ node.content
167
+ end
168
+
169
+ def lookup_feature(test_case)
170
+ if using_cucumber_messages?
171
+ @ast_lookup.gherkin_document(test_case.location.file)
172
+ else
173
+ test_case.feature
174
+ end
175
+ end
176
+
177
+ def lookup_test_case(test_case)
178
+ if using_cucumber_messages?
179
+ @ast_lookup.gherkin_document(test_case.location.file).feature
180
+ else
181
+ test_case
182
+ end
183
+ end
184
+
185
+ def lookup_step_source(step)
186
+ if using_cucumber_messages?
187
+ @ast_lookup.step_source(step).step
188
+ else
189
+ step.source.last
190
+ end
191
+ end
192
+
193
+ def current_feature(feature, clock)
194
+ if @feature&.feature == feature
195
+ @feature
196
+ else
197
+ feature_finished(clock)
198
+ @feature = Feature.new(feature, feature_started(feature, clock))
199
+ end
200
+ end
201
+
202
+ def hook?(test_step)
203
+ if using_cucumber_messages?
204
+ test_step.hook?
205
+ else
206
+ ! test_step.source.last.respond_to?(:keyword)
207
+ end
208
+ end
209
+
210
+ def status_to_level(status)
211
+ case status
212
+ when :passed
213
+ LOG_LEVELS[:info]
214
+ when :failed, :undefined, :pending, :error
215
+ LOG_LEVELS[:error]
216
+ when :skipped
217
+ LOG_LEVELS[:warn]
218
+ else
219
+ LOG_LEVELS.fetch(status, LOG_LEVELS[:info])
220
+ end
221
+ end
222
+
223
+
224
+ end
225
+ end
226
+ end