require'builder'require'fileutils'require'erb'moduleMinitestmoduleReporters# A reporter for generating HTML test reports# This is recommended to be used with a CI server, where the report is kept as an artifact and is accessible via# a shared link## The reporter sorts the results alphabetically and then by results# so that failing and skipped tests are at the top.## When using Minitest Specs, the number prefix is dropped from the name of the test so that it reads well## On each test run all files in the reports directory are deleted, this prevents a build up of old reports## The report is generated using ERB. A custom ERB template can be provided but it is not required# The default ERB template uses JQuery and Bootstrap, both of these are included by referencing the CDN sitesclassHtmlReporter<BaseReporter# The title of the reportattr_reader:title# The number of tests that passeddefpassescount-failures-errors-skipsend# The percentage of tests that passed, calculated in a way that avoids rounding errorsdefpercent_passes100-percent_skipps-percent_errors_failuresend# The percentage of tests that were skippeddefpercent_skipps(skips/count.to_f*100).to_iend# The percentage of tests that faileddefpercent_errors_failures((errors+failures)/count.to_f*100).to_iend# Trims off the number prefix on test names when using Minitest Specsdeffriendly_name(test)groups=test.name.scan(/(test_\d+_)(.*)/i)returntest.nameifgroups.empty?"it #{groups[0][1]}"end# The constructor takes a hash, and uses the following keys:# :title - the title that will be used in the report, defaults to 'Test Results'# :reports_dir - the directory the reports should be written to, defaults to 'test/html_reports'# :erb_template - the path to a custom ERB template, defaults to the supplied ERB template# :mode - Useful for debugging, :terse suppresses errors and is the default, :verbose lets errors bubble up# :output_filename - the report's filename, defaults to 'index.html'definitialize(args={})super({})defaults={:title=>'Test Results',:erb_template=>"#{File.dirname(__FILE__)}/../templates/index.html.erb",:reports_dir=>'test/html_reports',:mode=>:safe,:output_filename=>'index.html',}settings=defaults.merge(args)@mode=settings[:mode]@title=settings[:title]@erb_template=settings[:erb_template]@output_filename=settings[:output_filename]reports_dir=settings[:reports_dir]@reports_path=File.absolute_path(reports_dir)enddefstartsuperputs"Emptying #{@reports_path}"FileUtils.mkdir_p(@reports_path)File.delete(html_file)ifFile.exist?(html_file)end# Called by the framework to generate the reportdefreportsuperbeginputs"Writing HTML reports to #{@reports_path}"erb_str=File.read(@erb_template)renderer=ERB.new(erb_str)tests_by_suites=tests.group_by{|test|test_class(test)}# taken from the JUnit reportersuites=tests_by_suites.mapdo|suite,tests|suite_summary=summarize_suite(suite,tests)suite_summary[:tests]=tests.sort{|a,b|compare_tests(a,b)}suite_summaryendsuites.sort!{|a,b|compare_suites(a,b)}result=renderer.result(binding)File.open(html_file,'w')do|f|f.write(result)end# rubocop:disable Lint/RescueExceptionrescueException=>eputs'There was an error writing the HTML report'puts'This may have been caused by cancelling the test run'puts'Use mode => :verbose in the HTML reporters constructor to see more detail'if@mode==:terseputs'Use mode => :terse in the HTML reporters constructor to see less detail'if@mode!=:terseraiseeif@mode!=:terseend# rubocop:enable Lint/RescueExceptionendprivatedefhtml_file"#{@reports_path}/#{@output_filename}"enddefcompare_suites_by_name(suite_a,suite_b)suite_a[:name]<=>suite_b[:name]enddefcompare_tests_by_name(test_a,test_b)friendly_name(test_a)<=>friendly_name(test_b)end# Test suites are first ordered by evaluating the results of the tests, then by test suite name# Test suites which have failing tests are given highest order# Tests suites which have skipped tests are given second highest prioritydefcompare_suites(suite_a,suite_b)returncompare_suites_by_name(suite_a,suite_b)ifsuite_a[:has_errors_or_failures]&&suite_b[:has_errors_or_failures]return-1ifsuite_a[:has_errors_or_failures]&&!suite_b[:has_errors_or_failures]return1if!suite_a[:has_errors_or_failures]&&suite_b[:has_errors_or_failures]returncompare_suites_by_name(suite_a,suite_b)ifsuite_a[:has_skipps]&&suite_b[:has_skipps]return-1ifsuite_a[:has_skipps]&&!suite_b[:has_skipps]return1if!suite_a[:has_skipps]&&suite_b[:has_skipps]compare_suites_by_name(suite_a,suite_b)end# Tests are first ordered by evaluating the results of the tests, then by tests names# Tess which fail are given highest order# Tests which are skipped are given second highest prioritydefcompare_tests(test_a,test_b)returncompare_tests_by_name(test_a,test_b)iftest_fail_or_error?(test_a)&&test_fail_or_error?(test_b)return-1iftest_fail_or_error?(test_a)&&!test_fail_or_error?(test_b)return1if!test_fail_or_error?(test_a)&&test_fail_or_error?(test_b)returncompare_tests_by_name(test_a,test_b)iftest_a.skipped?&&test_b.skipped?return-1iftest_a.skipped?&&!test_b.skipped?return1if!test_a.skipped?&&test_b.skipped?compare_tests_by_name(test_a,test_b)enddeftest_fail_or_error?(test)test.error?||test.failureend# based on analyze_suite from the JUnit reporterdefsummarize_suite(suite,tests)summary=Hash.new(0)summary[:name]=suite.to_stests.eachdo|test|summary[:"#{result(test)}_count"]+=1summary[:assertion_count]+=test.assertionssummary[:test_count]+=1summary[:time]+=test.timeendsummary[:has_errors_or_failures]=(summary[:fail_count]+summary[:error_count])>0summary[:has_skipps]=summary[:skip_count]>0summaryend# based on message_for(test) from the JUnit reporterdefmessage_for(test)suite=test.classname=test.namee=test.failureiftest.passed?nilelsiftest.skipped?"Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"elsiftest.failure"Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"elsiftest.error?"Error:\n#{name}(#{suite}):\n#{e.message}"endend# taken from the JUnit reporterdeflocation(exception)last_before_assertion=''exception.backtrace.reverse_eachdo|s|breakifs=~/in .(assert|refute|flunk|pass|fail|raise|must|wont)/last_before_assertion=sendlast_before_assertion.sub(/:in .*$/,'')enddeftotal_time_to_hmsreturn('%.2fs'%total_time)iftotal_time<1hours=(total_time/(60*60)).roundminutes=((total_time/60)%60).round.to_s.rjust(2,'0')seconds=(total_time%60).round.to_s.rjust(2,'0')"#{hours}h#{minutes}m#{seconds}s"endendendend