├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── assets └── sample_report.png ├── bin ├── console └── setup ├── lib ├── minitest │ ├── hyper.rb │ ├── hyper │ │ ├── report.rb │ │ └── reporter.rb │ └── hyper_plugin.rb └── templates │ ├── hyper.css │ └── index.html.erb ├── minitest-hyper.gemspec └── test ├── minitest ├── hyper │ ├── report_test.rb │ └── reporter_test.rb └── hyper_test.rb ├── support ├── fake_reporter.rb └── filesystem_helpers.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor temporary files 2 | # 3 | *~ 4 | \#*\# 5 | .\#* 6 | *.swp 7 | *.swo 8 | 9 | 10 | # Other common temporary files 11 | # 12 | *.bak 13 | *.old 14 | *.orig 15 | *.rbc 16 | /tmp/* 17 | 18 | 19 | # Bundler artifacts 20 | # 21 | /.bundle/ 22 | /Gemfile.lock 23 | /vendor/bundle/ 24 | 25 | 26 | # RubyGems-related artifacts 27 | # 28 | /*.gem 29 | 30 | 31 | # Testing artifacts and temp files 32 | # 33 | /coverage/ 34 | /test/reports/ 35 | /test/tmp/ 36 | **/test/version_tmp 37 | 38 | 39 | # Documentation 40 | # 41 | /.yardoc 42 | /_yardoc/ 43 | /doc/ 44 | /rdoc/ 45 | 46 | 47 | # Keep files 48 | # 49 | !**/.gitkeep 50 | !**/.keep 51 | 52 | 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.3 4 | before_install: gem install bundler -v 1.10.6 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in minitest-hyper.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chris Kottom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minitest::Hyper 2 | 3 | Generates attractive, self-contained HTML reports for your Minitest runs. 4 | 5 | This gem was created as a demonstration of how to build a Minitest extension for [The Minitest Cookbook](http://chriskottom.com/minitestcookbook/). 6 | 7 |  8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem "minitest-hyper", require: false 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install minitest-hyper 24 | 25 | ## Usage 26 | 27 | To generate reports using Minitest::Hyper, you'll need to pass the `-H` or `--html` flag to Minitest on the command line in one of the usual ways. 28 | 29 | $ ruby -H test/foo_test.rb 30 | $ TEST_OPTS="--html" rake test 31 | 32 | You can also switch on report generation globally for every test run by putting the following in your test helper: 33 | 34 | ```ruby 35 | require "minitest/hyper" 36 | ``` 37 | 38 | ## Development 39 | 40 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 41 | 42 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 43 | 44 | ## Contributing 45 | 46 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/minitest-hyper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct. 47 | 48 | [The issue tracker for this project](https://github.com/[USERNAME]/minitest-hyper/issues) is the master list of enhancements and defects in need of repair. If you're looking to contribute, start there. 49 | 50 | ## License 51 | 52 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 53 | 54 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /assets/sample_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriskottom/minitest-hyper/1c23c68e6ac464ba1c4320b5c1ca0a2ddad2a84c/assets/sample_report.png -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "minitest/hyper" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/minitest/hyper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/hyper_plugin" 2 | 3 | Minitest.load_plugins 4 | Minitest::Hyper.enable! 5 | -------------------------------------------------------------------------------- /lib/minitest/hyper/report.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "fileutils" 3 | 4 | module Minitest 5 | module Hyper 6 | class Report 7 | attr_reader :reporter 8 | 9 | def initialize(reporter) 10 | @reporter = reporter 11 | end 12 | 13 | def write 14 | ensure_output_dir 15 | move_existing_file 16 | write_file 17 | end 18 | 19 | def url 20 | "file://#{ filename.gsub(/\\/, "/") }" 21 | end 22 | 23 | def dirname 24 | Minitest::Hyper.report_dirname 25 | end 26 | 27 | def filename 28 | Minitest::Hyper.report_filename 29 | end 30 | 31 | private 32 | 33 | def ensure_output_dir 34 | unless Dir.exist?(dirname) 35 | FileUtils.mkdir_p dirname 36 | end 37 | end 38 | 39 | def move_existing_file 40 | if File.exist?(filename) 41 | ctime = File.ctime(filename) 42 | time_str = ctime.strftime("%Y%m%d%H%M%S") 43 | new_name = filename.sub(/\.html$/, "_#{ time_str }.html") 44 | FileUtils.mv(filename, new_name) 45 | end 46 | end 47 | 48 | def write_file 49 | page_info = { 50 | title: "Minitest::Hyper Test Report", 51 | styles: css_string, 52 | timestamp: Time.now 53 | } 54 | test_info = reporter.to_h 55 | 56 | erb = ERB.new(template_string) 57 | File.open(filename, "wb") do |file| 58 | file.write erb.result(binding) 59 | end 60 | end 61 | 62 | def css_string 63 | File.read(CSS_TEMPLATE) 64 | end 65 | 66 | def template_string 67 | File.read(HTML_TEMPLATE) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/minitest/hyper/reporter.rb: -------------------------------------------------------------------------------- 1 | require "minitest" 2 | 3 | module Minitest 4 | module Hyper 5 | class Reporter < Minitest::StatisticsReporter 6 | attr_accessor :all_results 7 | 8 | def initialize(io = $stdout, options = {}) 9 | super 10 | self.all_results = [] 11 | end 12 | 13 | def record(result) 14 | super 15 | all_results << result 16 | end 17 | 18 | def report 19 | super 20 | @report = Report.new(self) 21 | @report.write 22 | io.puts "Wrote HTML test report to #{ @report.url }" 23 | end 24 | 25 | def to_h 26 | HashFormatter.as_hash(self) 27 | end 28 | 29 | class HashFormatter 30 | def self.as_hash(reporter) 31 | self.new(reporter).to_h 32 | end 33 | 34 | def initialize(reporter) 35 | @reporter = reporter 36 | end 37 | 38 | def reporter 39 | @reporter 40 | end 41 | 42 | def to_h 43 | { 44 | count: reporter.count, 45 | assertions: reporter.assertions, 46 | start_time: reporter.start_time, 47 | total_time: reporter.total_time, 48 | failures: reporter.failures, 49 | errors: reporter.errors, 50 | skips: reporter.skips, 51 | results: result_data(reporter.all_results), 52 | non_passing: result_data(reporter.results) 53 | } 54 | end 55 | 56 | def result_data(results) 57 | collection = [] 58 | results.each do |result| 59 | collection << { 60 | name: result.name, 61 | code: result_code(result), 62 | class: result_class(result), 63 | outcome: result_string(result), 64 | time: result.time, 65 | assertions: result.assertions, 66 | location: result.location, 67 | failure: result.failure 68 | } 69 | end 70 | collection 71 | end 72 | 73 | def result_code(result) 74 | case result.failure 75 | when Skip 76 | :skip 77 | when UnexpectedError 78 | :error 79 | when Assertion 80 | :fail 81 | else 82 | :pass 83 | end 84 | end 85 | 86 | def result_class(result) 87 | result_code(result).to_s 88 | end 89 | 90 | def result_string(result) 91 | result_code(result).to_s.capitalize 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/minitest/hyper_plugin.rb: -------------------------------------------------------------------------------- 1 | require "minitest" 2 | 3 | require "minitest/hyper/reporter" 4 | require "minitest/hyper/report" 5 | 6 | module Minitest 7 | # Mandatory Minitest initializer hook 8 | # Detected by Minitest.load_plugins, invoked during Minitest.init_plugins 9 | def self.plugin_hyper_init(options) 10 | if Hyper.enabled? 11 | reporter.reporters << Hyper::Reporter.new(options[:io], options) 12 | end 13 | end 14 | 15 | # Optional hook for command line params handling 16 | # Invoked by Minitest.process_args 17 | def self.plugin_hyper_options(opts, options) 18 | description = "Generate an HTML test report with Minitest::Hyper" 19 | opts.on "-H", "--html", description do 20 | Hyper.enable! 21 | end 22 | end 23 | 24 | module Hyper 25 | GEM_DIR = File.join(File.dirname(__FILE__), "../..") 26 | 27 | TEMPLATE_DIR = File.join(GEM_DIR, "lib/templates") 28 | CSS_TEMPLATE = File.join(TEMPLATE_DIR, "hyper.css") 29 | HTML_TEMPLATE = File.join(TEMPLATE_DIR, "index.html.erb") 30 | 31 | VERSION = "0.2.0" 32 | 33 | @@enabled = false 34 | 35 | def self.enabled? 36 | @@enabled 37 | end 38 | 39 | def self.enable! 40 | @@enabled = true 41 | end 42 | 43 | def self.report_dirname 44 | project_root = if defined?(Rails) && defined?(Rails.root) 45 | Rails.root 46 | else 47 | Dir.pwd 48 | end 49 | 50 | if Dir.exist?(File.join(project_root, "spec")) 51 | File.join(project_root, "spec/reports/hyper") 52 | else 53 | File.join(project_root, "test/reports/hyper") 54 | end 55 | end 56 | 57 | def self.report_filename 58 | File.join(report_dirname, "index.html") 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/templates/hyper.css: -------------------------------------------------------------------------------- 1 | body { padding-top: 70px; } 2 | 3 | .row { 4 | margin-bottom: 20px; 5 | } 6 | 7 | /* 8 | * SUMMARY ROW 9 | */ 10 | #summary .widget { 11 | border: 3px solid #eef; 12 | border-radius: 10px; 13 | margin-right: 2.5%; 14 | padding: 10px; 15 | width: 18%; 16 | } 17 | 18 | #summary .widget:last-child { 19 | margin-right: 0; 20 | } 21 | 22 | #summary .counter { 23 | display: block; 24 | font-size: 4em; 25 | font-weight: bold; 26 | text-align: center; 27 | } 28 | 29 | #summary .metric { 30 | display: block; 31 | font-size: 1.1em; 32 | font-weight: bold; 33 | text-align: center; 34 | } 35 | 36 | 37 | /* 38 | * INDIVIDUAL TEST RESULTS 39 | */ 40 | #test-results { 41 | margin: 0; 42 | } 43 | 44 | #test-results .result { 45 | border: solid; 46 | border-width: 3px thin; 47 | font-size: 14px; 48 | margin-bottom: 3px; 49 | padding: 6px; 50 | width: 100%; 51 | } 52 | 53 | #test-results .result span.outcome-name, 54 | #test-results .result span.time { 55 | display: inline-block; 56 | font-size: 15px; 57 | font-weight: bold; 58 | } 59 | 60 | #test-results .result span.outcome-name { 61 | overflow: hidden; 62 | max-width: 85%; 63 | white-space: nowrap; 64 | text-overflow: ellipsis; 65 | } 66 | 67 | #test-results .result span.time { 68 | float: right; 69 | width: 15%; 70 | } 71 | 72 | #test-results .result .failure-details { 73 | clear: both; 74 | } 75 | 76 | #test-results .result.pass { 77 | background-color: #eef; 78 | border-color: green; 79 | font-color: dark-green; 80 | } 81 | 82 | #test-results .result.fail { 83 | background-color: #ffe4b2; 84 | border-color: orange; 85 | } 86 | 87 | #test-results .result.error { 88 | background-color: #fdd; 89 | border-color: red; 90 | } 91 | 92 | #test-results .result.skip { 93 | background-color: #f0f0f0; 94 | border-color: #aaa; 95 | } 96 | 97 | 98 | /* 99 | * CHARTS 100 | */ 101 | #charts h3 { 102 | margin: 0; 103 | text-align: center; 104 | } 105 | 106 | #charts canvas { 107 | display: block; 108 | height: 300px; 109 | margin: 0 auto; 110 | width: 90%; 111 | } 112 | -------------------------------------------------------------------------------- /lib/templates/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |