parallel_report_portal 2.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.
@@ -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