reportportal 0.7

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,63 @@
1
+ # Copyright 2015 EPAM Systems
2
+ #
3
+ #
4
+ # This file is part of Report Portal.
5
+ #
6
+ # Report Portal is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ReportPortal is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with Report Portal. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ # monkey-patch to temporarily solve the issue in https://github.com/rest-client/rest-client/pull/222
20
+ module RestClient
21
+ module Payload
22
+ extend self
23
+ class Multipart < Base
24
+ def build_stream(params)
25
+ content_type = params.delete(:content_type)
26
+ b = '--' + boundary
27
+
28
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
29
+ @stream.binmode
30
+ @stream.write(b + EOL)
31
+
32
+ case params
33
+ when Hash, ParamsArray
34
+ x = Utils.flatten_params(params)
35
+ else
36
+ x = params
37
+ end
38
+
39
+ last_index = x.length - 1
40
+ x.each_with_index do |a, index|
41
+ k, v = * a
42
+ if v.respond_to?(:read) && v.respond_to?(:path)
43
+ create_file_field(@stream, k, v)
44
+ else
45
+ create_regular_field(@stream, k, v, content_type)
46
+ end
47
+ @stream.write(EOL + b)
48
+ @stream.write(EOL) unless last_index == index
49
+ end
50
+ @stream.write('--')
51
+ @stream.write(EOL)
52
+ @stream.seek(0)
53
+ end
54
+
55
+ def create_regular_field(s, k, v, type = nil)
56
+ s.write("Content-Disposition: form-data; name=\"#{k}\"")
57
+ s.write("#{EOL}Content-Type: #{type}#{EOL}") if type
58
+ s.write(EOL)
59
+ s.write(v)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,130 @@
1
+ # Copyright 2015 EPAM Systems
2
+ #
3
+ #
4
+ # This file is part of Report Portal.
5
+ #
6
+ # Report Portal is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ReportPortal is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with Report Portal. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'securerandom'
20
+ require 'tree'
21
+ require 'rspec/core'
22
+
23
+ require_relative '../../reportportal'
24
+
25
+ # TODO: Screenshots
26
+ # TODO: Logs
27
+ module ReportPortal
28
+ module RSpec
29
+ class Formatter
30
+ MAX_DESCRIPTION_LENGTH = 255
31
+ MIN_DESCRIPTION_LENGTH = 3
32
+
33
+ ::RSpec::Core::Formatters.register self, :start, :example_group_started, :example_group_finished,
34
+ :example_started, :example_passed, :example_failed,
35
+ :example_pending, :message, :stop
36
+
37
+ def initialize(_output)
38
+ ENV['REPORT_PORTAL_USED'] = 'true'
39
+ end
40
+
41
+ def start(_start_notification)
42
+ cmd_args = ARGV.map { |arg| (arg.include? 'rp_uuid=')? 'rp_uuid=[FILTERED]' : arg }.join(' ')
43
+ ReportPortal.start_launch(cmd_args)
44
+ @root_node = Tree::TreeNode.new(SecureRandom.hex)
45
+ @current_group_node = @root_node
46
+ end
47
+
48
+ def example_group_started(group_notification)
49
+ description = group_notification.group.description
50
+ if description.size < MIN_DESCRIPTION_LENGTH
51
+ p "Group description should be at least #{MIN_DESCRIPTION_LENGTH} characters ('group_notification': #{group_notification.inspect})"
52
+ return
53
+ end
54
+ item = ReportPortal::TestItem.new(description[0..MAX_DESCRIPTION_LENGTH-1],
55
+ :TEST,
56
+ nil,
57
+ ReportPortal.now,
58
+ '',
59
+ false,
60
+ [])
61
+ group_node = Tree::TreeNode.new(SecureRandom.hex, item)
62
+ if group_node.nil?
63
+ p "Group node is nil for item #{item.inspect}"
64
+ else
65
+ @current_group_node << group_node unless @current_group_node.nil? # make @current_group_node parent of group_node
66
+ @current_group_node = group_node
67
+ group_node.content.id = ReportPortal.start_item(group_node)
68
+ end
69
+ end
70
+
71
+ def example_group_finished(_group_notification)
72
+ unless @current_group_node.nil?
73
+ ReportPortal.finish_item(@current_group_node.content)
74
+ @current_group_node = @current_group_node.parent
75
+ end
76
+ end
77
+
78
+ def example_started(notification)
79
+ description = notification.example.description
80
+ if description.size < MIN_DESCRIPTION_LENGTH
81
+ p "Example description should be at least #{MIN_DESCRIPTION_LENGTH} characters ('notification': #{notification.inspect})"
82
+ return
83
+ end
84
+ ReportPortal.current_scenario = ReportPortal::TestItem.new(description[0..MAX_DESCRIPTION_LENGTH-1],
85
+ :STEP,
86
+ nil,
87
+ ReportPortal.now,
88
+ '',
89
+ false,
90
+ [])
91
+ example_node = Tree::TreeNode.new(SecureRandom.hex, ReportPortal.current_scenario)
92
+ if example_node.nil?
93
+ p "Example node is nil for scenario #{ReportPortal.current_scenario.inspect}"
94
+ else
95
+ @current_group_node << example_node
96
+ example_node.content.id = ReportPortal.start_item(example_node)
97
+ end
98
+ end
99
+
100
+ def example_passed(_notification)
101
+ ReportPortal.finish_item(ReportPortal.current_scenario, :passed) unless ReportPortal.current_scenario.nil?
102
+ ReportPortal.current_scenario = nil
103
+ end
104
+
105
+ def example_failed(notification)
106
+ exception = notification.exception
107
+ ReportPortal.send_log(:failed, %(#{exception.class}: #{exception.message}\n\nStacktrace: #{notification.formatted_backtrace.join("\n")}), ReportPortal.now)
108
+ ReportPortal.finish_item(ReportPortal.current_scenario, :failed) unless ReportPortal.current_scenario.nil?
109
+ ReportPortal.current_scenario = nil
110
+ end
111
+
112
+ def example_pending(_notification)
113
+ ReportPortal.finish_item(ReportPortal.current_scenario, :skipped) unless ReportPortal.current_scenario.nil?
114
+ ReportPortal.current_scenario = nil
115
+ end
116
+
117
+ def message(notification)
118
+ if notification.message.respond_to?(:read)
119
+ ReportPortal.send_file(:passed, notification.message)
120
+ else
121
+ ReportPortal.send_log(:passed, notification.message, ReportPortal.now)
122
+ end
123
+ end
124
+
125
+ def stop(_notification)
126
+ ReportPortal.finish_launch
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,76 @@
1
+ # Copyright 2015 EPAM Systems
2
+ #
3
+ #
4
+ # This file is part of Report Portal.
5
+ #
6
+ # Report Portal is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ReportPortal is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with Report Portal. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'yaml'
20
+ require 'singleton'
21
+
22
+ module ReportPortal
23
+ class Settings
24
+ include Singleton
25
+
26
+ PREFIX = 'rp_'
27
+
28
+ def initialize
29
+ filename = ENV.fetch("#{PREFIX}config") do
30
+ glob = Dir.glob('{,.config/,config/}report{,-,_}portal{.yml,.yaml}')
31
+ p "Multiple configuration files found for ReportPortal. Using the first one: #{glob.first}" if glob.size > 1
32
+ glob.first
33
+ end
34
+
35
+ @properties = filename.nil? ? {} : YAML.load_file(filename)
36
+ keys = {
37
+ 'uuid' => true,
38
+ 'endpoint' => true,
39
+ 'project' => true,
40
+ 'launch' => true,
41
+ 'description' => false,
42
+ 'tags' => false,
43
+ 'is_debug' => false,
44
+ 'disable_ssl_verification' => false,
45
+ # for parallel execution only
46
+ 'use_standard_logger' => false,
47
+ 'launch_id' => false,
48
+ 'file_with_launch_id' => false,
49
+ }
50
+
51
+ keys.each do |key, is_required|
52
+ define_singleton_method(key.to_sym) { setting(key) }
53
+ fail "ReportPortal: Define environment variable '#{PREFIX}#{key}' or key #{key} in the configuration YAML file" if is_required && public_send(key).nil?
54
+ end
55
+ end
56
+
57
+ def launch_mode
58
+ is_debug ? 'DEBUG' : 'DEFAULT'
59
+ end
60
+
61
+ def formatter_modes
62
+ setting('formatter_modes') || []
63
+ end
64
+
65
+ def project_url
66
+ "#{endpoint}/#{project}"
67
+ end
68
+
69
+ private
70
+
71
+ def setting(key)
72
+ pkey = PREFIX + key
73
+ ENV.key?(pkey) ? YAML.load(ENV[pkey]) : @properties[key]
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
1
+ # Copyright 2015 EPAM Systems
2
+ #
3
+ #
4
+ # This file is part of Report Portal.
5
+ #
6
+ # Report Portal is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ReportPortal is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with Report Portal. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'rake'
20
+ require 'pathname'
21
+ require 'tempfile'
22
+ require_relative '../reportportal'
23
+
24
+ namespace :reportportal do
25
+ desc 'Start launch in Report Portal and print its id to $stdout (needed for use with ReportPortal::Cucumber::AttachToLaunchFormatter)'
26
+ task :start_launch do
27
+ description = ENV['description']
28
+ file_to_write_launch_id = ENV.fetch('file_for_launch_id') { Pathname(Dir.tmpdir) + 'rp_launch_id.tmp' }
29
+ launch_id = ReportPortal.start_launch(description)
30
+ File.write(file_to_write_launch_id, launch_id)
31
+ puts launch_id
32
+ end
33
+
34
+ desc 'Finish launch in Report Portal (needed for use with ReportPortal::Cucumber::AttachToLaunchFormatter)'
35
+ task :finish_launch do
36
+ launch_id = ENV['launch_id']
37
+ file_with_launch_id = ENV['file_with_launch_id']
38
+ puts "Launch id isn't provided. Provide it either via launch_id or file_with_launch_id environment variables" if !launch_id && !file_with_launch_id
39
+ puts "Both launch_id and file_with_launch_id are present in environment variables" if launch_id && file_with_launch_id
40
+ ReportPortal.launch_id = launch_id || File.read(file_with_launch_id)
41
+ ReportPortal.close_child_items(nil)
42
+ ReportPortal.finish_launch
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright 2015 EPAM Systems
2
+ #
3
+ #
4
+ # This file is part of Report Portal.
5
+ #
6
+ # Report Portal is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ReportPortal is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with Report Portal. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ module ReportPortal
20
+ VERSION = '0.7'
21
+ end
@@ -0,0 +1,208 @@
1
+ # Copyright 2015 EPAM Systems
2
+ #
3
+ #
4
+ # This file is part of Report Portal.
5
+ #
6
+ # Report Portal is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ReportPortal is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with Report Portal. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'json'
20
+ require 'rest_client'
21
+ require 'uri'
22
+ require 'pathname'
23
+ require 'tempfile'
24
+
25
+ require_relative 'report_portal/settings'
26
+ require_relative 'report_portal/patches/rest_client'
27
+
28
+ module ReportPortal
29
+ TestItem = Struct.new(:name, :type, :id, :start_time, :description, :closed, :tags)
30
+ LOG_LEVELS = { error: 'ERROR', warn: 'WARN', info: 'INFO', debug: 'DEBUG', trace: 'TRACE', fatal: 'FATAL', unknown: 'UNKNOWN' }
31
+
32
+ @response_handler = proc do |response, request, result, &block|
33
+ if (200..207).include? response.code
34
+ response
35
+ else
36
+ p "ReportPortal API returned #{response}"
37
+ p "Offending request method/URL: #{request.args[:method].upcase} #{request.args[:url]}"
38
+ p "Offending request payload: #{request.args[:payload]}}"
39
+ response.return!(request, result, &block)
40
+ end
41
+ end
42
+
43
+ class << self
44
+ attr_accessor :launch_id, :current_scenario
45
+
46
+ def now
47
+ (Time.now.to_f * 1000).to_i
48
+ end
49
+
50
+ def status_to_level(status)
51
+ case status
52
+ when :passed
53
+ LOG_LEVELS[:info]
54
+ when :failed, :undefined, :pending, :error
55
+ LOG_LEVELS[:error]
56
+ when :skipped
57
+ LOG_LEVELS[:warn]
58
+ else
59
+ LOG_LEVELS.fetch(status, LOG_LEVELS[:info])
60
+ end
61
+ end
62
+
63
+ def start_launch(description, start_time = now)
64
+ url = "#{Settings.instance.project_url}/launch"
65
+ data = { name: Settings.instance.launch, start_time: start_time, tags: Settings.instance.tags, description: description, mode: Settings.instance.launch_mode }
66
+ @launch_id = do_request(url) do |resource|
67
+ JSON.parse(resource.post(data.to_json, content_type: :json, &@response_handler))['id']
68
+ end
69
+ end
70
+
71
+ def finish_launch(end_time = now)
72
+ url = "#{Settings.instance.project_url}/launch/#{@launch_id}/finish"
73
+ data = { end_time: end_time }
74
+ do_request(url) do |resource|
75
+ resource.put data.to_json, content_type: :json, &@response_handler
76
+ end
77
+ end
78
+
79
+ def start_item(item_node)
80
+ url = "#{Settings.instance.project_url}/item"
81
+ url += "/#{item_node.parent.content.id}" unless item_node.parent && item_node.parent.is_root?
82
+ item = item_node.content
83
+ data = { start_time: item.start_time, name: item.name[0, 255], type: item.type.to_s, launch_id: @launch_id, description: item.description }
84
+ data[:tags] = item.tags unless item.tags.empty?
85
+ do_request(url) do |resource|
86
+ JSON.parse(resource.post(data.to_json, content_type: :json, &@response_handler))['id']
87
+ end
88
+ end
89
+
90
+ def finish_item(item, status = nil, end_time = nil, force_issue = nil)
91
+ unless item.nil? || item.id.nil? || item.closed
92
+ url = "#{Settings.instance.project_url}/item/#{item.id}"
93
+ data = { end_time: end_time.nil? ? now : end_time }
94
+ data[:status] = status unless status.nil?
95
+ if force_issue && status != :passed # TODO: check for :passed status is probably not needed
96
+ data[:issue] = { issue_type: 'AUTOMATION_BUG', comment: force_issue.to_s }
97
+ elsif status == :skipped
98
+ data[:issue] = { issue_type: 'NOT_ISSUE' }
99
+ end
100
+ do_request(url) do |resource|
101
+ resource.put data.to_json, content_type: :json, &@response_handler
102
+ end
103
+ item.closed = true
104
+ end
105
+ end
106
+
107
+ # TODO: implement force finish
108
+
109
+ def send_log(status, message, time)
110
+ unless @current_scenario.nil? || @current_scenario.closed # it can be nil if scenario outline in expand mode is executed
111
+ url = "#{Settings.instance.project_url}/log"
112
+ data = { item_id: @current_scenario.id, time: time, level: status_to_level(status), message: message.to_s }
113
+ do_request(url) do |resource|
114
+ resource.post(data.to_json, content_type: :json, &@response_handler)
115
+ end
116
+ end
117
+ end
118
+
119
+ def send_file(status, path, label = nil, time = now, mime_type='image/png')
120
+ url = "#{Settings.instance.project_url}/log"
121
+ unless File.file?(path)
122
+ extension = ".#{MIME::Types[mime_type].first.extensions.first}"
123
+ temp = Tempfile.open(['file',extension])
124
+ temp.binmode
125
+ temp.write(Base64.decode64(path))
126
+ temp.rewind
127
+ path = temp
128
+ end
129
+ File.open(File.realpath(path), 'rb') do |file|
130
+ label ||= File.basename(file)
131
+ json = { level: status_to_level(status), message: label, item_id: @current_scenario.id, time: time, file: { name: File.basename(file) } }
132
+ data = { :json_request_part => [json].to_json, label => file, :multipart => true, :content_type => 'application/json' }
133
+ do_request(url) do |resource|
134
+ resource.post(data, { content_type: 'multipart/form-data' }, &@response_handler)
135
+ end
136
+ end
137
+ end
138
+
139
+ # needed for parallel formatter
140
+ def item_id_of(name, parent_node)
141
+ if parent_node.is_root? # folder without parent folder
142
+ url = "#{Settings.instance.project_url}/item?filter.eq.launch=#{@launch_id}&filter.eq.name=#{URI.escape(name)}&filter.size.path=0"
143
+ else
144
+ url = "#{Settings.instance.project_url}/item?filter.eq.parent=#{parent_node.content.id}&filter.eq.name=#{URI.escape(name)}"
145
+ end
146
+ do_request(url) do |resource|
147
+ data = JSON.parse(resource.get)
148
+ if data.key? 'content'
149
+ data['content'].empty? ? nil : data['content'][0]['id']
150
+ else
151
+ nil # item isn't started yet
152
+ end
153
+ end
154
+ end
155
+
156
+ # needed for parallel formatter
157
+ def close_child_items(parent_id)
158
+ if parent_id.nil?
159
+ url = "#{Settings.instance.project_url}/item?filter.eq.launch=#{@launch_id}&filter.size.path=0&page.page=1&page.size=100"
160
+ else
161
+ url = "#{Settings.instance.project_url}/item?filter.eq.parent=#{parent_id}&page.page=1&page.size=100"
162
+ end
163
+ ids = []
164
+ loop do
165
+ response = do_request(url) { |r| JSON.parse(r.get) }
166
+ if response.key?('links')
167
+ link = response['links'].find { |i| i['rel'] == 'next' }
168
+ url = link.nil? ? nil : link['href']
169
+ else
170
+ url = nil
171
+ end
172
+ response['content'].each do |i|
173
+ ids << i['id'] if i['has_childs'] && i['status'] == 'IN_PROGRESS'
174
+ end
175
+ break if url.nil?
176
+ end
177
+
178
+ ids.each do |id|
179
+ close_child_items(id)
180
+ # temporary, we actually only need the id
181
+ finish_item(TestItem.new(nil, nil, id, nil, nil, nil, nil))
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def create_resource(url)
188
+ props = { :headers => {:Authorization => "Bearer #{Settings.instance.uuid}"}}
189
+ verify_ssl = Settings.instance.disable_ssl_verification
190
+ props[:verify_ssl] = !verify_ssl unless verify_ssl.nil?
191
+ RestClient::Resource.new url, props
192
+ end
193
+
194
+ def do_request(url)
195
+ resource = create_resource(url)
196
+ tries = 3
197
+ begin
198
+ yield resource
199
+ rescue
200
+ p "Request to #{url} produced an exception: #{$!.class}: #{$!}"
201
+ $!.backtrace.each { |l| p l }
202
+ retry unless (tries -= 1).zero?
203
+ p "Failed to execute request to #{url} after 3 attempts."
204
+ nil
205
+ end
206
+ end
207
+ end
208
+ end