reportportal 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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