Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions lib/report_portal/cucumber/extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
module ReportPortal
module Cucumber
# extract interesting data from a cucumber v4+ event
class ExtractorCucumber4
def initialize(config)
require 'cucumber/formatter/ast_lookup'
@ast_lookup = ::Cucumber::Formatter::AstLookup.new(config)
end

# For Cucumber4 we actually return a representation of the gherkin
# document. So when querying tags, keywords etc, these need to come
# from the 'feature' attribute of the gherkin document
def feature(test_case)
@ast_lookup.gherkin_document(test_case.location.file)
end

def feature_location(gherkin)
gherkin.uri
end

def feature_tags(gherkin)
gherkin.feature.tags
end

def feature_name(gherkin)
"#{gherkin.feature.keyword}: #{gherkin.feature.name}"
end

def same_feature_as_previous_test_case?(previous_name, gherkin)
previous_name == gherkin.uri.split(File::SEPARATOR).last
end

def scenario_keyword(test_case)
@ast_lookup.scenario_source(test_case).scenario.keyword
end

def scenario_name(test_case)
@ast_lookup.scenario_source(test_case).scenario.name
end

def step_source(test_step)
@ast_lookup.step_source(test_step).step
end

def step_multiline_arg(test_step)
test_step.multiline_arg
end

def step_backtrace_line(test_step)
test_step.backtrace_line
end

def step_type(test_step)
case step?(test_step)
when true
'Step'
when false
"#{test_step.text} at #{test_step.location}"
end
end

def step?(test_step)
!test_step.hook?
end
end

# extract interesting data from a cucumber v3 event
class ExtractorCucumber3
def initialize(*)
require 'cucumber/formatter/hook_query_visitor'
end

def feature(test_case)
test_case.feature
end

def feature_location(feature)
feature.location.file
end

def feature_tags(feature)
feature.tags
end

def feature_name(feature)
"#{feature.keyword}: #{feature.name}"
end

def same_feature_as_previous_test_case?(previous_name, feature)
previous_name == feature.location.file.split(File::SEPARATOR).last
end

def scenario_keyword(test_case)
test_case.keyword
end

def scenario_name(test_case)
test_case.name
end

def step_source(test_step)
test_step.source.last
end

def step_multiline_arg(test_step)
step_source(test_step).multiline_arg
end

def step_backtrace_line(test_step)
test_step.source.last.backtrace_line
end

def step_type(test_step)
case step?(test_step)
when true
'Step'
when false
hook_class_name = test_step.source.last.class.name.split('::').last
"#{hook_class_name} at #{test_step.location}"
end
end

def step?(test_step)
!::Cucumber::Formatter::HookQueryVisitor.new(test_step).hook?
end
end

# Simple factory to provide an implementation that can extract interesting
# data from a cucumber event
class Extractor
def self.create(config)
if (::Cucumber::VERSION.split('.').map(&:to_i) <=> [4, 0, 0]).positive?
ExtractorCucumber4.new(config)
else
ExtractorCucumber3.new(config)
end
end
end
end
end
24 changes: 20 additions & 4 deletions lib/report_portal/cucumber/formatter.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
require_relative 'report'
require_relative 'extractor'

module ReportPortal
module Cucumber
class Formatter
# @api private
def initialize(config)
ENV['REPORT_PORTAL_USED'] = 'true'
@config = config
# Helper class used to abstract away the internal
# differences of cucumber 3 / 4+
@extractor = Extractor.create(config)

setup_message_processing

@io = config.out_stream
case config.out_stream
when IO
@io = config.out_stream
when String
@io = File.open(config.out_stream, 'w')
end

%i[test_case_started test_case_finished test_step_started test_step_finished test_run_finished].each do |event_name|
config.on_event event_name do |event|
Expand All @@ -21,18 +31,24 @@ def initialize(config)

def puts(message)
process_message(:puts, message)
@io.puts(message)
@io.flush
@io&.puts(message)
@io&.flush
end

def embed(*args)
process_message(:embed, *args)
end

# embed is deprecated from cucumber4, should use attach
# instead
def attach(*args)
process_message(:attach, *args)
end

private

def report
@report ||= ReportPortal::Cucumber::Report.new
@report ||= ReportPortal::Cucumber::Report.new(@extractor)
end

def setup_message_processing
Expand Down
64 changes: 31 additions & 33 deletions lib/report_portal/cucumber/report.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require 'cucumber/formatter/io'
require 'cucumber/formatter/hook_query_visitor'
require 'tree'
require 'securerandom'

Expand All @@ -18,7 +17,8 @@ def attach_to_launch?
ReportPortal::Settings.instance.formatter_modes.include?('attach_to_launch')
end

def initialize
def initialize(extractor)
@extractor = extractor
@last_used_time = 0
@root_node = Tree::TreeNode.new('')
@parent_item_node = @root_node
Expand All @@ -45,13 +45,13 @@ def start_launch(desired_time = ReportPortal.now)
# TODO: time should be a required argument
def test_case_started(event, desired_time = ReportPortal.now)
test_case = event.test_case
feature = test_case.feature
if report_hierarchy? && !same_feature_as_previous_test_case?(feature)
feature = @extractor.feature(test_case)
if report_hierarchy? && !@extractor.same_feature_as_previous_test_case?(@parent_item_node.name, feature)
end_feature(desired_time) unless @parent_item_node.is_root?
start_feature_with_parentage(feature, desired_time)
end

name = "#{test_case.keyword}: #{test_case.name}"
name = "#{@extractor.scenario_keyword(test_case)}: #{@extractor.scenario_name(test_case)}"
description = test_case.location.to_s
tags = test_case.tags.map(&:name)
type = :STEP
Expand All @@ -76,13 +76,14 @@ def test_case_finished(event, desired_time = ReportPortal.now)

def test_step_started(event, desired_time = ReportPortal.now)
test_step = event.test_step
if step?(test_step) # `after_test_step` is also invoked for hooks
step_source = test_step.source.last
if @extractor.step?(test_step) # `after_test_step` is also invoked for hooks
step_source = @extractor.step_source(test_step)
message = "-- #{step_source.keyword}#{step_source.text} --"
if step_source.multiline_arg.doc_string?
message << %(\n"""\n#{step_source.multiline_arg.content}\n""")
elsif step_source.multiline_arg.data_table?
message << step_source.multiline_arg.raw.reduce("\n") { |acc, row| acc << "| #{row.join(' | ')} |\n" }
multiline_arg = @extractor.step_multiline_arg(test_step)
if multiline_arg.doc_string?
message << %(\n"""\n#{multiline_arg.content}\n""")
elsif multiline_arg.data_table?
message << multiline_arg.raw.reduce("\n") { |acc, row| acc << "| #{row.join(' | ')} |\n" }
end
ReportPortal.send_log(:trace, message, time_to_send(desired_time))
end
Expand All @@ -98,20 +99,14 @@ def test_step_finished(event, desired_time = ReportPortal.now)
ex = result.exception
format("%s: %s\n %s", ex.class.name, ex.message, ex.backtrace.join("\n "))
else
format("Undefined step: %s:\n%s", test_step.text, test_step.source.last.backtrace_line)
format("Undefined step: %s:\n%s", test_step.text, @extractor.step_backtrace_line(test_step))
end
ReportPortal.send_log(:error, exception_info, time_to_send(desired_time))
end

if status != :passed
log_level = status == :skipped ? :warn : :error
step_type = if step?(test_step)
'Step'
else
hook_class_name = test_step.source.last.class.name.split('::').last
location = test_step.location
"#{hook_class_name} at `#{location}`"
end
step_type = @extractor.step_type(test_step)
ReportPortal.send_log(log_level, "#{step_type} #{status}", time_to_send(desired_time))
end
end
Expand All @@ -134,6 +129,17 @@ def embed(path_or_src, mime_type, label, desired_time = ReportPortal.now)
ReportPortal.send_file(:info, path_or_src, label, time_to_send(desired_time), mime_type)
end

def attach(path_or_src, mime_type, desired_time = ReportPortal.now)
# Cucumber > 4 has deprecated the use of puts, and instead wants
# the use of "log". This in turn calls attach on all formatters
# with mime-type 'text/x.cucumber.log+plain'
if mime_type == 'text/x.cucumber.log+plain'
ReportPortal.send_log(:info, path_or_src, time_to_send(desired_time))
else
ReportPortal.send_file(:info, path_or_src, nil, time_to_send(desired_time), mime_type)
end
end

private

# Report Portal sorts logs by time. However, several logs might have the same time.
Expand All @@ -150,14 +156,10 @@ def time_to_send(desired_time)
@last_used_time = time_to_send
end

def same_feature_as_previous_test_case?(feature)
@parent_item_node.name == feature.location.file.split(File::SEPARATOR).last
end

def start_feature_with_parentage(feature, desired_time)
parent_node = @root_node
child_node = nil
path_components = feature.location.file.split(File::SEPARATOR)
path_components = @extractor.feature_location(feature).split(File::SEPARATOR)
path_components.each_with_index do |path_component, index|
child_node = parent_node[path_component]
unless child_node # if child node was not created yet
Expand All @@ -167,15 +169,15 @@ def start_feature_with_parentage(feature, desired_time)
tags = []
type = :SUITE
else
name = "#{feature.keyword}: #{feature.name}"
description = feature.file # TODO: consider adding feature description and comments
tags = feature.tags.map(&:name)
name = @extractor.feature_name(feature)
description = @extractor.feature_location(feature) # TODO: consider adding feature description and comments
tags = @extractor.feature_tags(feature).map(&:name)
type = :TEST
end
# TODO: multithreading # Parallel formatter always executes scenarios inside the same feature in the same process
if parallel? &&
if (parallel? || attach_to_launch?) &&
index < path_components.size - 1 && # is folder?
(id_of_created_item = ReportPortal.item_id_of(name, parent_node)) # get id for folder from report portal
(id_of_created_item = ReportPortal.uuid_of(name, parent_node)) # get id for folder from report portal
# get child id from other process
item = ReportPortal::TestItem.new(name: name, type: type, id: id_of_created_item, start_time: time_to_send(desired_time), description: description, closed: false, tags: tags)
child_node = Tree::TreeNode.new(path_component, item)
Expand Down Expand Up @@ -208,10 +210,6 @@ def close_all_children_of(root_node)
end
end

def step?(test_step)
!::Cucumber::Formatter::HookQueryVisitor.new(test_step).hook?
end

def report_hierarchy?
!ReportPortal::Settings.instance.formatter_modes.include?('skip_reporting_hierarchy')
end
Expand Down
2 changes: 1 addition & 1 deletion lib/report_portal/http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def create_client
def add_insecure_ssl_options
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
@http.default_options = { ssl_context: ssl_context }
@http.default_options = @http.default_options.with_ssl_context(ssl_context)
end

# Response should be consumed before sending next request via the same persistent connection.
Expand Down
Loading