require 'multi_json' require 'base64' require 'cucumber/formatter/io' require 'cucumber/formatter/hook_query_visitor' module Cucumber module Formatter # The formatter used for --format json class Json include Io def initialize(runtime, io, _options) @runtime = runtime @io = ensure_io(io) @feature_hashes = [] end def before_test_case(test_case) builder = Builder.new(test_case) unless same_feature_as_previous_test_case?(test_case.feature) @feature_hash = builder.feature_hash @feature_hashes << @feature_hash end @test_case_hash = builder.test_case_hash if builder.background? feature_elements << builder.background_hash @element_hash = builder.background_hash else feature_elements << @test_case_hash @element_hash = @test_case_hash end @any_step_failed = false end def before_test_step(test_step) return if internal_hook?(test_step) hook_query = HookQueryVisitor.new(test_step) if hook_query.hook? @step_or_hook_hash = {} hooks_of_type(hook_query) << @step_or_hook_hash return end if first_step_after_background?(test_step) feature_elements << @test_case_hash @element_hash = @test_case_hash end @step_or_hook_hash = create_step_hash(test_step.source.last) steps << @step_or_hook_hash @step_hash = @step_or_hook_hash end def after_test_step(test_step, result) return if internal_hook?(test_step) add_match_and_result(test_step, result) @any_step_failed = true if result.failed? end def after_test_case(test_case, result) add_failed_around_hook(result) if result.failed? && !@any_step_failed end def done @io.write(MultiJson.dump(@feature_hashes, pretty: true)) end def puts(message) test_step_output << message end def embed(src, mime_type, _label) if File.file?(src) content = File.open(src, 'rb') { |f| f.read } data = encode64(content) else if mime_type =~ /;base64$/ mime_type = mime_type[0..-8] data = src else data = encode64(src) end end test_step_embeddings << { mime_type: mime_type, data: data } end private def same_feature_as_previous_test_case?(feature) current_feature[:uri] == feature.file && current_feature[:line] == feature.location.line end def first_step_after_background?(test_step) test_step.source[1].name != @element_hash[:name] end def internal_hook?(test_step) test_step.source.last.location.file.include?('lib/cucumber/') end def current_feature @feature_hash ||= {} end def feature_elements @feature_hash[:elements] ||= [] end def steps @element_hash[:steps] ||= [] end def hooks_of_type(hook_query) case hook_query.type when :before return before_hooks when :after return after_hooks when :after_step return after_step_hooks else fail 'Unkown hook type ' + hook_query.type.to_s end end def before_hooks @element_hash[:before] ||= [] end def after_hooks @element_hash[:after] ||= [] end def around_hooks @element_hash[:around] ||= [] end def after_step_hooks @step_hash[:after] ||= [] end def test_step_output @step_or_hook_hash[:output] ||= [] end def test_step_embeddings @step_or_hook_hash[:embeddings] ||= [] end def create_step_hash(step_source) step_hash = { keyword: step_source.keyword, name: step_source.name, line: step_source.location.line } step_hash[:comments] = Formatter.create_comments_array(step_source.comments) unless step_source.comments.empty? step_hash[:doc_string] = create_doc_string_hash(step_source.multiline_arg) if step_source.multiline_arg.doc_string? step_hash end def create_doc_string_hash(doc_string) { value: doc_string.content, content_type: doc_string.content_type, line: doc_string.location.line } end def add_match_and_result(test_step, result) @step_or_hook_hash[:match] = create_match_hash(test_step, result) @step_or_hook_hash[:result] = create_result_hash(result) end def add_failed_around_hook(result) @step_or_hook_hash = {} around_hooks << @step_or_hook_hash @step_or_hook_hash[:match] = { location: "unknown_hook_location:1" } @step_or_hook_hash[:result] = create_result_hash(result) end def create_match_hash(test_step, result) { location: test_step.action_location } end def create_result_hash(result) result_hash = { status: result.to_sym } result_hash[:error_message] = create_error_message(result) if result.failed? || result.pending? result.duration.tap { |duration| result_hash[:duration] = duration.nanoseconds } result_hash end def create_error_message(result) message_element = result.failed? ? result.exception : result message = "#{message_element.message} (#{message_element.class})" ([message] + message_element.backtrace).join("\n") end def encode64(data) # strip newlines from the encoded data Base64.encode64(data).gsub(/\n/, '') end class Builder attr_reader :feature_hash, :background_hash, :test_case_hash def initialize(test_case) @background_hash = nil test_case.describe_source_to(self) test_case.feature.background.describe_to(self) end def background? @background_hash != nil end def feature(feature) @feature_hash = { uri: feature.file, id: create_id(feature), keyword: feature.keyword, name: feature.name, description: feature.description, line: feature.location.line } unless feature.tags.empty? @feature_hash[:tags] = create_tags_array(feature.tags) if @test_case_hash[:tags] @test_case_hash[:tags] = @feature_hash[:tags] + @test_case_hash[:tags] else @test_case_hash[:tags] = @feature_hash[:tags] end end @feature_hash[:comments] = Formatter.create_comments_array(feature.comments) unless feature.comments.empty? @test_case_hash[:id].insert(0, @feature_hash[:id] + ';') end def background(background) @background_hash = { keyword: background.keyword, name: background.name, description: background.description, line: background.location.line, type: 'background' } @background_hash[:comments] = Formatter.create_comments_array(background.comments) unless background.comments.empty? end def scenario(scenario) @test_case_hash = { id: create_id(scenario), keyword: scenario.keyword, name: scenario.name, description: scenario.description, line: scenario.location.line, type: 'scenario' } @test_case_hash[:tags] = create_tags_array(scenario.tags) unless scenario.tags.empty? @test_case_hash[:comments] = Formatter.create_comments_array(scenario.comments) unless scenario.comments.empty? end def scenario_outline(scenario) @test_case_hash = { id: create_id(scenario) + ';' + @example_id, keyword: scenario.keyword, name: scenario.name, description: scenario.description, line: @row.location.line, type: 'scenario' } tags = [] tags += create_tags_array(scenario.tags) unless scenario.tags.empty? tags += @examples_table_tags if @examples_table_tags @test_case_hash[:tags] = tags unless tags.empty? comments = [] comments += Formatter.create_comments_array(scenario.comments) unless scenario.comments.empty? comments += @examples_table_comments if @examples_table_comments comments += @row_comments if @row_comments @test_case_hash[:comments] = comments unless comments.empty? end def examples_table(examples_table) # the json file have traditionally used the header row as row 1, # wheras cucumber-ruby-core used the first example row as row 1. @example_id = create_id(examples_table) + ";#{@row.number + 1}" @examples_table_tags = create_tags_array(examples_table.tags) unless examples_table.tags.empty? @examples_table_comments = Formatter.create_comments_array(examples_table.comments) unless examples_table.comments.empty? end def examples_table_row(row) @row = row @row_comments = Formatter.create_comments_array(row.comments) unless row.comments.empty? end private def create_id(element) element.name.downcase.gsub(/ /, '-') end def create_tags_array(tags) tags_array = [] tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } } tags_array end end end def self.create_comments_array(comments) comments_array = [] comments.each { |comment| comments_array << { value: comment.to_s.strip, line: comment.location.line } } comments_array end end end