# frozen_string_literal: true require 'json' module Danger # Shows all build errors, warnings and unit tests results generated from `xcodebuild`. # You need to use [xcpretty](https://github.com/supermarin/xcpretty) # with [xcpretty-json-formatter](https://github.com/marcelofabri/xcpretty-json-formatter) # to generate a JSON file that this plugin can read. # @example Showing summary # # xcode_summary.report 'xcodebuild.json' # # @example Filtering warnings in Pods # # xcode_summary.ignored_files = '**/Pods/**' # xcode_summary.report 'xcodebuild.json' # # @see diogot/danger-xcode_summary # @tags xcode, xcodebuild, format # class DangerXcodeSummary < Plugin Location = Struct.new(:file_name, :file_path, :line) Result = Struct.new(:message, :location) # The project root, which will be used to make the paths relative. # Defaults to `pwd`. # @param [String] value # @return [String] attr_accessor :project_root # A globbed string or array of strings which should match the files # that you want to ignore warnings on. Defaults to nil. # An example would be `'**/Pods/**'` to ignore warnings in Pods that your project uses. # # @param [String or [String]] value # @return [[String]] attr_accessor :ignored_files # A block that filters specific results. # An example would be `lambda { |result| result.message.start_with?('ld') }` to ignore results for ld_warnings. # # @param [Block value # @return [Block] attr_accessor :ignored_results # Defines if the test summary will be sticky or not. # Defaults to `false`. # @param [Boolean] value # @return [Boolean] attr_accessor :sticky_summary # Defines if the build summary is shown or not. # Defaults to `true`. # @param [Boolean] value # @return [Boolean] attr_accessor :test_summary # Defines if using inline comment or not. # Defaults to `false`. # @param [Boolean] value # @return [Boolean] attr_accessor :inline_mode # Defines if warnings should be included or not # Defaults to `false`. # @param [Boolean] value # @return [Boolean] attr_accessor :ignores_warnings # rubocop:disable Lint/DuplicateMethods def project_root root = @project_root || Dir.pwd root += '/' unless root.end_with? '/' root end def ignored_files [@ignored_files].flatten.compact end def ignored_results(&block) @ignored_results ||= block end def sticky_summary @sticky_summary || false end def test_summary @test_summary.nil? ? true : @test_summary end def inline_mode @inline_mode || false end def ignores_warnings @ignores_warnings || false end # rubocop:enable Lint/DuplicateMethods # Reads a file with JSON Xcode summary and reports it. # # @param [String] file_path Path for Xcode summary in JSON format. # @return [void] def report(file_path) if File.file?(file_path) xcode_summary = JSON.parse(File.read(file_path), symbolize_names: true) format_summary(xcode_summary) else fail 'summary file not found' end end # Reads a file with JSON Xcode summary and reports its warning and error count. # # @param [String] file_path Path for Xcode summary in JSON format. # @return [String] JSON string with warningCount and errorCount def warning_error_count(file_path) if File.file?(file_path) xcode_summary = JSON.parse(File.read(file_path), symbolize_names: true) warning_count = warnings(xcode_summary).count error_count = errors(xcode_summary).count result = { warnings: warning_count, errors: error_count } result.to_json else fail 'summary file not found' end end private def format_summary(xcode_summary) messages(xcode_summary).each { |s| message(s, sticky: sticky_summary) } warnings(xcode_summary).each do |result| if inline_mode && result.location warn(result.message, sticky: false, file: result.location.file_name, line: result.location.line) else warn(result.message, sticky: false) end end errors(xcode_summary).each do |result| if inline_mode && result.location fail(result.message, sticky: false, file: result.location.file_name, line: result.location.line) else fail(result.message, sticky: false) end end end def messages(xcode_summary) if test_summary [ xcode_summary[:tests_summary_messages] ].flatten.uniq.compact.map(&:strip) else [] end end def warnings(xcode_summary) if ignores_warnings return [] end warnings = [ xcode_summary.fetch(:warnings, []).map { |message| Result.new(message, nil) }, xcode_summary.fetch(:ld_warnings, []).map { |message| Result.new(message, nil) }, xcode_summary.fetch(:compile_warnings, {}).map do |h| Result.new(format_compile_warning(h), parse_location(h)) end ].flatten.uniq.compact.reject { |result| result.message.nil? } warnings.delete_if(&ignored_results) end def errors(xcode_summary) errors = [ xcode_summary.fetch(:errors, []).map { |message| Result.new(message, nil) }, xcode_summary.fetch(:compile_errors, {}).map do |h| Result.new(format_compile_warning(h), parse_location(h)) end, xcode_summary.fetch(:file_missing_errors, {}).map do |h| Result.new(format_format_file_missing_error(h), parse_location(h)) end, xcode_summary.fetch(:undefined_symbols_errors, {}).map do |h| Result.new(format_undefined_symbols(h), nil) end, xcode_summary.fetch(:duplicate_symbols_errors, {}).map do |h| Result.new(format_duplicate_symbols(h), nil) end, xcode_summary.fetch(:tests_failures, {}).map do |test_suite, failures| failures.map do |failure| Result.new(format_test_failure(test_suite, failure), parse_test_location(failure)) end end ].flatten.uniq.compact.reject { |result| result.message.nil? } errors.delete_if(&ignored_results) end def parse_location(input) file_path, line, _column = input[:file_path].split(':') Location.new(input[:file_name], file_path, line.to_i) end def parse_test_location(failure) path, line = failure[:file_path].split(':') file_name = relative_path(path) Location.new(file_name, path, line.to_i) end def format_path(path) # Pick a Dangerfile plugin for a chosen request_source # based on https://github.com/danger/danger/blob/master/lib/danger/plugin_support/plugin.rb#L31 plugins = Plugin.all_plugins.select { |plugin| Dangerfile.essential_plugin_classes.include? plugin } plugin = plugins.select { |p| p.method_defined? :html_link }.map { |p| p.new(@dangerfile) }.compact.first if plugin clean_path, line = parse_filename(path) path = clean_path + '#L' + line if clean_path && line plugin.html_link(path) else path end end def parse_filename(path) regex = /^(.*?):(\d*):?\d*$/ match = path.match(regex) match&.captures end def relative_path(path) return nil if project_root.nil? path.gsub(project_root, '') end def should_ignore_warning?(path) parsed = parse_filename(path) path = parsed.first || path ignored_files.any? { |pattern| File.fnmatch(pattern, path) } end def escape_reason(reason) reason.gsub('>', '\>').gsub('<', '\<') end def format_compile_warning(input) path = relative_path(input[:file_path]) return nil if should_ignore_warning?(path) path_link = format_path(path) warning = "**#{path_link}**: #{escape_reason(input[:reason])}
" if input[:line] && !input[:line].empty? "#{warning}" \ "```\n" \ "#{input[:line]}\n" \ '```' else warning end end def format_format_file_missing_error(input) path = relative_path(input[:file_path]) path_link = format_path(path) "**#{escape_reason(input[:reason])}**: #{path_link}" end def format_undefined_symbols(input) "#{input[:message]}
" \ "> Symbol: #{input[:symbol]}
" \ "> Referenced from: #{input[:reference]}" end def format_duplicate_symbols(input) "#{input[:message]}
" \ "> #{input[:file_paths].map { |path| path.split('/').last }.join('
')}" end def format_test_failure(suite_name, failure) path = relative_path(failure[:file_path]) path_link = format_path(path) "**#{suite_name}**: #{failure[:test_case]}, #{escape_reason(failure[:reason])}
#{path_link}" end end end