minitest-heat 0.0.6 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/lib/minitest/heat/backtrace/line.rb +118 -0
- data/lib/minitest/heat/backtrace.rb +48 -14
- data/lib/minitest/heat/hit.rb +28 -27
- data/lib/minitest/heat/issue.rb +92 -73
- data/lib/minitest/heat/location.rb +71 -53
- data/lib/minitest/heat/map.rb +14 -17
- data/lib/minitest/heat/output/backtrace.rb +28 -13
- data/lib/minitest/heat/output/issue.rb +93 -23
- data/lib/minitest/heat/output/map.rb +47 -24
- data/lib/minitest/heat/output/marker.rb +5 -3
- data/lib/minitest/heat/output/results.rb +32 -21
- data/lib/minitest/heat/output/source_code.rb +1 -1
- data/lib/minitest/heat/output/token.rb +2 -1
- data/lib/minitest/heat/output.rb +70 -2
- data/lib/minitest/heat/results.rb +27 -81
- data/lib/minitest/heat/timer.rb +81 -0
- data/lib/minitest/heat/version.rb +1 -1
- data/lib/minitest/heat.rb +3 -2
- data/lib/minitest/heat_plugin.rb +9 -17
- data/lib/minitest/heat_reporter.rb +29 -23
- metadata +4 -3
- data/lib/minitest/heat/line.rb +0 -74
| @@ -22,10 +22,10 @@ module Minitest | |
| 22 22 | 
             
                    end
         | 
| 23 23 | 
             
                  end
         | 
| 24 24 |  | 
| 25 | 
            -
                  attr_reader : | 
| 25 | 
            +
                  attr_reader :test_definition_location, :backtrace
         | 
| 26 26 |  | 
| 27 | 
            -
                  def initialize( | 
| 28 | 
            -
                    @ | 
| 27 | 
            +
                  def initialize(test_definition_location, backtrace = [])
         | 
| 28 | 
            +
                    @test_definition_location = TestDefinition.new(*test_definition_location)
         | 
| 29 29 | 
             
                    @backtrace = Backtrace.new(backtrace)
         | 
| 30 30 | 
             
                  end
         | 
| 31 31 |  | 
| @@ -45,9 +45,9 @@ module Minitest | |
| 45 45 | 
             
                  #   test, and it raises an exception, then it's really a broken test rather than a proper
         | 
| 46 46 | 
             
                  #   faiure.
         | 
| 47 47 | 
             
                  #
         | 
| 48 | 
            -
                  # @return [Boolean] true if  | 
| 48 | 
            +
                  # @return [Boolean] true if final file in the backtrace is the same as the test location file
         | 
| 49 49 | 
             
                  def broken_test?
         | 
| 50 | 
            -
                    !test_file.nil? && test_file ==  | 
| 50 | 
            +
                    !test_file.nil? && test_file == final_file
         | 
| 51 51 | 
             
                  end
         | 
| 52 52 |  | 
| 53 53 | 
             
                  # Knows if the failure occurred in the actual project source code—as opposed to the test or
         | 
| @@ -59,6 +59,15 @@ module Minitest | |
| 59 59 | 
             
                    !source_code_file.nil? && !broken_test?
         | 
| 60 60 | 
             
                  end
         | 
| 61 61 |  | 
| 62 | 
            +
             | 
| 63 | 
            +
             | 
| 64 | 
            +
                  # The final location of the stacktrace regardless of whether it's from within the project
         | 
| 65 | 
            +
                  #
         | 
| 66 | 
            +
                  # @return [String] the relative path to the file from the project root
         | 
| 67 | 
            +
                  def final_file
         | 
| 68 | 
            +
                    Pathname(final_location.pathname)
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 62 71 | 
             
                  # The file most likely to be the source of the underlying problem. Often, the most recent
         | 
| 63 72 | 
             
                  #   backtrace files will be a gem or external library that's failing indirectly as a result
         | 
| 64 73 | 
             
                  #   of a problem with local source code (not always, but frequently). In that case, the best
         | 
| @@ -69,78 +78,76 @@ module Minitest | |
| 69 78 | 
             
                    Pathname(most_relevant_location.pathname)
         | 
| 70 79 | 
             
                  end
         | 
| 71 80 |  | 
| 72 | 
            -
                  # The  | 
| 81 | 
            +
                  # The final location from the stacktrace that is a test file
         | 
| 73 82 | 
             
                  #
         | 
| 74 | 
            -
                  # @return [ | 
| 75 | 
            -
                  def  | 
| 76 | 
            -
                     | 
| 83 | 
            +
                  # @return [String, nil] the relative path to the file from the project root
         | 
| 84 | 
            +
                  def test_file
         | 
| 85 | 
            +
                    Pathname(final_test_location.pathname)
         | 
| 77 86 | 
             
                  end
         | 
| 78 87 |  | 
| 79 | 
            -
                  # The final location  | 
| 88 | 
            +
                  # The final location from the stacktrace that is within the project directory
         | 
| 80 89 | 
             
                  #
         | 
| 81 | 
            -
                  # @return [String] the relative path to the file from the project root
         | 
| 82 | 
            -
                  def  | 
| 83 | 
            -
                     | 
| 84 | 
            -
                  end
         | 
| 90 | 
            +
                  # @return [String, nil] the relative path to the file from the project root
         | 
| 91 | 
            +
                  def source_code_file
         | 
| 92 | 
            +
                    return nil if final_source_code_location.nil?
         | 
| 85 93 |  | 
| 86 | 
            -
             | 
| 87 | 
            -
                  #
         | 
| 88 | 
            -
                  # @return [Integer] line number
         | 
| 89 | 
            -
                  def final_failure_line
         | 
| 90 | 
            -
                    final_location.line_number
         | 
| 94 | 
            +
                    Pathname(final_source_code_location.pathname)
         | 
| 91 95 | 
             
                  end
         | 
| 92 96 |  | 
| 93 | 
            -
                  # The final location of the stacktrace  | 
| 97 | 
            +
                  # The final location of the stacktrace from within the project (source code or test code)
         | 
| 94 98 | 
             
                  #
         | 
| 95 99 | 
             
                  # @return [String] the relative path to the file from the project root
         | 
| 96 100 | 
             
                  def project_file
         | 
| 97 | 
            -
                     | 
| 101 | 
            +
                    return nil if project_location.nil?
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    Pathname(project_location.pathname)
         | 
| 98 104 | 
             
                  end
         | 
| 99 105 |  | 
| 100 | 
            -
             | 
| 106 | 
            +
             | 
| 107 | 
            +
                  # The line number of the `final_file` where the failure originated
         | 
| 101 108 | 
             
                  #
         | 
| 102 109 | 
             
                  # @return [Integer] line number
         | 
| 103 | 
            -
                  def  | 
| 104 | 
            -
                     | 
| 110 | 
            +
                  def final_failure_line
         | 
| 111 | 
            +
                    final_location.line_number
         | 
| 105 112 | 
             
                  end
         | 
| 106 113 |  | 
| 107 | 
            -
                  # The  | 
| 114 | 
            +
                  # The line number of the `most_relevant_file` where the failure originated
         | 
| 108 115 | 
             
                  #
         | 
| 109 | 
            -
                  # @return [ | 
| 110 | 
            -
                  def  | 
| 111 | 
            -
                     | 
| 112 | 
            -
             | 
| 113 | 
            -
                    backtrace.final_source_code_location.pathname
         | 
| 116 | 
            +
                  # @return [Integer] line number
         | 
| 117 | 
            +
                  def most_relevant_failure_line
         | 
| 118 | 
            +
                    most_relevant_location.line_number
         | 
| 114 119 | 
             
                  end
         | 
| 115 120 |  | 
| 116 | 
            -
                  # The line number of the ` | 
| 121 | 
            +
                  # The line number of the `test_file` where the test is defined
         | 
| 117 122 | 
             
                  #
         | 
| 118 123 | 
             
                  # @return [Integer] line number
         | 
| 119 | 
            -
                  def  | 
| 120 | 
            -
                     | 
| 121 | 
            -
             | 
| 122 | 
            -
                    backtrace.final_source_code_location.line_number
         | 
| 124 | 
            +
                  def test_definition_line
         | 
| 125 | 
            +
                    test_definition_location.line_number
         | 
| 123 126 | 
             
                  end
         | 
| 124 127 |  | 
| 125 | 
            -
                  # The  | 
| 128 | 
            +
                  # The line number from within the `test_file` test definition where the failure occurred
         | 
| 126 129 | 
             
                  #
         | 
| 127 | 
            -
                  # @return [ | 
| 128 | 
            -
                  def  | 
| 129 | 
            -
                     | 
| 130 | 
            +
                  # @return [Integer] line number
         | 
| 131 | 
            +
                  def test_failure_line
         | 
| 132 | 
            +
                    final_test_location.line_number
         | 
| 130 133 | 
             
                  end
         | 
| 131 134 |  | 
| 132 | 
            -
                  # The line number of the ` | 
| 135 | 
            +
                  # The line number of the `source_code_file` where the failure originated
         | 
| 133 136 | 
             
                  #
         | 
| 134 137 | 
             
                  # @return [Integer] line number
         | 
| 135 | 
            -
                  def  | 
| 136 | 
            -
                     | 
| 138 | 
            +
                  def source_code_failure_line
         | 
| 139 | 
            +
                    final_source_code_location&.line_number
         | 
| 137 140 | 
             
                  end
         | 
| 138 141 |  | 
| 139 | 
            -
                  # The line number  | 
| 142 | 
            +
                  # The line number of the `project_file` where the failure originated
         | 
| 140 143 | 
             
                  #
         | 
| 141 144 | 
             
                  # @return [Integer] line number
         | 
| 142 | 
            -
                  def  | 
| 143 | 
            -
                     | 
| 145 | 
            +
                  def project_failure_line
         | 
| 146 | 
            +
                    if !broken_test? && !source_code_file.nil?
         | 
| 147 | 
            +
                      source_code_failure_line
         | 
| 148 | 
            +
                    else
         | 
| 149 | 
            +
                      test_failure_line
         | 
| 150 | 
            +
                    end
         | 
| 144 151 | 
             
                  end
         | 
| 145 152 |  | 
| 146 153 | 
             
                  # The line number from within the `test_file` test definition where the failure occurred
         | 
| @@ -148,7 +155,7 @@ module Minitest | |
| 148 155 | 
             
                  # @return [Location] the last location from the backtrace or the test location if a backtrace
         | 
| 149 156 | 
             
                  #   was not passed to the initializer
         | 
| 150 157 | 
             
                  def final_location
         | 
| 151 | 
            -
                    backtrace? ? backtrace.final_location :  | 
| 158 | 
            +
                    backtrace.parsed_entries.any? ? backtrace.final_location : test_definition_location
         | 
| 152 159 | 
             
                  end
         | 
| 153 160 |  | 
| 154 161 | 
             
                  # The file most likely to be the source of the underlying problem. Often, the most recent
         | 
| @@ -159,22 +166,33 @@ module Minitest | |
| 159 166 | 
             
                  # @return [Array] file and line number of the most likely source of the problem
         | 
| 160 167 | 
             
                  def most_relevant_location
         | 
| 161 168 | 
             
                    [
         | 
| 162 | 
            -
                       | 
| 163 | 
            -
                       | 
| 169 | 
            +
                      final_source_code_location,
         | 
| 170 | 
            +
                      final_test_location,
         | 
| 164 171 | 
             
                      final_location
         | 
| 165 172 | 
             
                    ].compact.first
         | 
| 166 173 | 
             
                  end
         | 
| 167 174 |  | 
| 168 | 
            -
                   | 
| 169 | 
            -
             | 
| 175 | 
            +
                  # Returns the final test location based on the backtrace if present. Otherwise falls back to
         | 
| 176 | 
            +
                  #   the test location which represents the test definition.
         | 
| 177 | 
            +
                  #
         | 
| 178 | 
            +
                  # @return [Location] the final location from the test files
         | 
| 179 | 
            +
                  def final_test_location
         | 
| 180 | 
            +
                    backtrace.final_test_location || test_definition_location
         | 
| 170 181 | 
             
                  end
         | 
| 171 182 |  | 
| 172 | 
            -
                   | 
| 183 | 
            +
                  # Returns the final source code location based on the backtrace
         | 
| 184 | 
            +
                  #
         | 
| 185 | 
            +
                  # @return [Location] the final location from the source code files
         | 
| 186 | 
            +
                  def final_source_code_location
         | 
| 173 187 | 
             
                    backtrace.final_source_code_location
         | 
| 174 188 | 
             
                  end
         | 
| 175 189 |  | 
| 176 | 
            -
                   | 
| 177 | 
            -
             | 
| 190 | 
            +
                  # Returns the final project location based on the backtrace if present. Otherwise falls back
         | 
| 191 | 
            +
                  #   to the test location which represents the test definition.
         | 
| 192 | 
            +
                  #
         | 
| 193 | 
            +
                  # @return [Location] the final location from the project files
         | 
| 194 | 
            +
                  def project_location
         | 
| 195 | 
            +
                    backtrace.final_project_location || test_definition_location
         | 
| 178 196 | 
             
                  end
         | 
| 179 197 | 
             
                end
         | 
| 180 198 | 
             
              end
         | 
    
        data/lib/minitest/heat/map.rb
    CHANGED
    
    | @@ -2,43 +2,40 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Minitest
         | 
| 4 4 | 
             
              module Heat
         | 
| 5 | 
            +
                # Structured approach to collecting the locations of issues for generating a heat map
         | 
| 5 6 | 
             
                class Map
         | 
| 6 7 | 
             
                  MAXIMUM_FILES_TO_SHOW = 5
         | 
| 7 8 |  | 
| 8 9 | 
             
                  attr_reader :hits
         | 
| 9 10 |  | 
| 10 | 
            -
                  # So we can sort hot spots by liklihood of being the most important spot to check out before
         | 
| 11 | 
            -
                  #   trying to fix something. These are ranked based on the possibility they represent ripple
         | 
| 12 | 
            -
                  #   effects where fixing one problem could potentially fix multiple other failures.
         | 
| 13 | 
            -
                  #
         | 
| 14 | 
            -
                  #   For example, if there's an exception in the file, start there. Broken code can't run. If a
         | 
| 15 | 
            -
                  #   test is broken (i.e. raising an exception), that's a special sort of failure that would be
         | 
| 16 | 
            -
                  #   misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
         | 
| 17 | 
            -
                  WEIGHTS = {
         | 
| 18 | 
            -
                    error: 3,    # exceptions from source code have the highest liklihood of a ripple effect
         | 
| 19 | 
            -
                    broken: 2,   # broken tests won't have ripple effects but can't help if they can't run
         | 
| 20 | 
            -
                    failure: 1,  # failures are kind of the whole point, and they could have ripple effects
         | 
| 21 | 
            -
                    skipped: 0,  # skips aren't failures, but they shouldn't go ignored
         | 
| 22 | 
            -
                    painful: 0,  # slow tests aren't failures, but they shouldn't be ignored
         | 
| 23 | 
            -
                    slow: 0
         | 
| 24 | 
            -
                  }.freeze
         | 
| 25 | 
            -
             | 
| 26 11 | 
             
                  def initialize
         | 
| 27 12 | 
             
                    @hits = {}
         | 
| 28 13 | 
             
                  end
         | 
| 29 14 |  | 
| 15 | 
            +
                  # Records a hit to the list of files and issue types
         | 
| 16 | 
            +
                  # @param filename [String] the unique path and file name for recordings hits
         | 
| 17 | 
            +
                  # @param line_number [Integer] the line number where the issue was encountered
         | 
| 18 | 
            +
                  # @param type [Symbol] the type of issue that was encountered (i.e. :failure, :error, etc.)
         | 
| 19 | 
            +
                  #
         | 
| 20 | 
            +
                  # @return [void]
         | 
| 30 21 | 
             
                  def add(filename, line_number, type)
         | 
| 31 22 | 
             
                    @hits[filename] ||= Hit.new(filename)
         | 
| 32 23 |  | 
| 33 | 
            -
                    @hits[filename].log(type, line_number)
         | 
| 24 | 
            +
                    @hits[filename].log(type.to_sym, line_number)
         | 
| 34 25 | 
             
                  end
         | 
| 35 26 |  | 
| 27 | 
            +
                  # Returns a subset of affected files to keep the list from being overwhelming
         | 
| 28 | 
            +
                  #
         | 
| 29 | 
            +
                  # @return [Array] the list of files and the line numbers for each encountered issue type
         | 
| 36 30 | 
             
                  def file_hits
         | 
| 37 31 | 
             
                    hot_files.take(MAXIMUM_FILES_TO_SHOW)
         | 
| 38 32 | 
             
                  end
         | 
| 39 33 |  | 
| 40 34 | 
             
                  private
         | 
| 41 35 |  | 
| 36 | 
            +
                  # Sorts the files by hit "weight" so that the most problematic files are at the beginning
         | 
| 37 | 
            +
                  #
         | 
| 38 | 
            +
                  # @return [Array] the collection of files that encountred issues
         | 
| 42 39 | 
             
                  def hot_files
         | 
| 43 40 | 
             
                    hits.values.sort_by(&:weight).reverse
         | 
| 44 41 | 
             
                  end
         | 
| @@ -5,7 +5,7 @@ module Minitest | |
| 5 5 | 
             
                class Output
         | 
| 6 6 | 
             
                  # Builds the collection of tokens for a backtrace when an exception occurs
         | 
| 7 7 | 
             
                  class Backtrace
         | 
| 8 | 
            -
                    DEFAULT_LINE_COUNT =  | 
| 8 | 
            +
                    DEFAULT_LINE_COUNT = 10
         | 
| 9 9 | 
             
                    DEFAULT_INDENTATION_SPACES = 2
         | 
| 10 10 |  | 
| 11 11 | 
             
                    attr_accessor :location, :backtrace
         | 
| @@ -23,13 +23,9 @@ module Minitest | |
| 23 23 | 
             
                      # Iterate over the selected lines from the backtrace
         | 
| 24 24 | 
             
                      backtrace_entries.each do |backtrace_entry|
         | 
| 25 25 | 
             
                        # Get the source code for the line from the backtrace
         | 
| 26 | 
            -
                        parts =  | 
| 27 | 
            -
                          indentation_token,
         | 
| 28 | 
            -
                          path_token(backtrace_entry),
         | 
| 29 | 
            -
                          file_and_line_number_token(backtrace_entry),
         | 
| 30 | 
            -
                          source_code_line_token(backtrace_entry.source_code)
         | 
| 31 | 
            -
                        ]
         | 
| 26 | 
            +
                        parts = backtrace_line_tokens(backtrace_entry)
         | 
| 32 27 |  | 
| 28 | 
            +
                        # If it's the most recently modified file in the trace, add the token for that
         | 
| 33 29 | 
             
                        parts << file_freshness(backtrace_entry) if most_recently_modified?(backtrace_entry)
         | 
| 34 30 |  | 
| 35 31 | 
             
                        @tokens << parts
         | 
| @@ -52,11 +48,20 @@ module Minitest | |
| 52 48 | 
             
                    # ...it's smart about exceptions that were raised outside of the project?
         | 
| 53 49 | 
             
                    # ...it's smart about highlighting lines of code differently based on whether it's source code, test code, or external code?
         | 
| 54 50 | 
             
                    def backtrace_entries
         | 
| 55 | 
            -
                       | 
| 51 | 
            +
                      all_entries
         | 
| 56 52 | 
             
                    end
         | 
| 57 53 |  | 
| 58 54 | 
             
                    private
         | 
| 59 55 |  | 
| 56 | 
            +
                    def backtrace_line_tokens(backtrace_entry)
         | 
| 57 | 
            +
                      [
         | 
| 58 | 
            +
                        indentation_token,
         | 
| 59 | 
            +
                        path_token(backtrace_entry),
         | 
| 60 | 
            +
                        *file_and_line_number_tokens(backtrace_entry),
         | 
| 61 | 
            +
                        source_code_line_token(backtrace_entry.source_code)
         | 
| 62 | 
            +
                      ]
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
             | 
| 60 65 | 
             
                    def all_backtrace_entries_from_project?
         | 
| 61 66 | 
             
                      backtrace_entries.all? { |line| line.path.to_s.include?(project_root_dir) }
         | 
| 62 67 | 
             
                    end
         | 
| @@ -83,25 +88,31 @@ module Minitest | |
| 83 88 | 
             
                    end
         | 
| 84 89 |  | 
| 85 90 | 
             
                    def path_token(line)
         | 
| 91 | 
            +
                      style = line.to_s.include?(Dir.pwd) ? :default : :muted
         | 
| 86 92 | 
             
                      path = "#{line.path}/"
         | 
| 87 93 |  | 
| 88 94 | 
             
                      # If all of the backtrace lines are from the project, no point in the added redundant
         | 
| 89 95 | 
             
                      #  noise of showing the project root directory over and over again
         | 
| 90 96 | 
             
                      path = path.delete_prefix(project_root_dir) if all_backtrace_entries_from_project?
         | 
| 91 97 |  | 
| 92 | 
            -
                      [ | 
| 98 | 
            +
                      [style, path]
         | 
| 93 99 | 
             
                    end
         | 
| 94 100 |  | 
| 95 | 
            -
                    def  | 
| 96 | 
            -
                       | 
| 101 | 
            +
                    def file_and_line_number_tokens(backtrace_entry)
         | 
| 102 | 
            +
                      style = backtrace_entry.to_s.include?(Dir.pwd) ? :bold : :muted
         | 
| 103 | 
            +
                      [
         | 
| 104 | 
            +
                        [style, backtrace_entry.file],
         | 
| 105 | 
            +
                        [:muted, ':'],
         | 
| 106 | 
            +
                        [style, backtrace_entry.line_number]
         | 
| 107 | 
            +
                      ]
         | 
| 97 108 | 
             
                    end
         | 
| 98 109 |  | 
| 99 110 | 
             
                    def source_code_line_token(source_code)
         | 
| 100 | 
            -
                      [:muted, " `#{source_code.line.strip}`"]
         | 
| 111 | 
            +
                      [:muted, " #{Output::SYMBOLS[:arrow]} `#{source_code.line.strip}`"]
         | 
| 101 112 | 
             
                    end
         | 
| 102 113 |  | 
| 103 114 | 
             
                    def file_freshness(_line)
         | 
| 104 | 
            -
                      [: | 
| 115 | 
            +
                      [:default, " #{Output::SYMBOLS[:middot]} Most Recently Modified File"]
         | 
| 105 116 | 
             
                    end
         | 
| 106 117 |  | 
| 107 118 | 
             
                    # The number of spaces each line of code should be indented. Currently defaults to 2 in
         | 
| @@ -115,6 +126,10 @@ module Minitest | |
| 115 126 | 
             
                    def indentation
         | 
| 116 127 | 
             
                      DEFAULT_INDENTATION_SPACES
         | 
| 117 128 | 
             
                    end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    def style_for(path)
         | 
| 131 | 
            +
                      path.to_s.include?(Dir.pwd) ? :default : :muted
         | 
| 132 | 
            +
                    end
         | 
| 118 133 | 
             
                  end
         | 
| 119 134 | 
             
                end
         | 
| 120 135 | 
             
              end
         | 
| @@ -3,12 +3,8 @@ | |
| 3 3 | 
             
            module Minitest
         | 
| 4 4 | 
             
              module Heat
         | 
| 5 5 | 
             
                class Output
         | 
| 6 | 
            -
                   | 
| 7 | 
            -
             | 
| 8 | 
            -
                      spacer: ' · ',
         | 
| 9 | 
            -
                      arrow: ' > '
         | 
| 10 | 
            -
                    }.freeze
         | 
| 11 | 
            -
             | 
| 6 | 
            +
                  # Formats issues to output based on the issue type
         | 
| 7 | 
            +
                  class Issue # rubocop:disable Metrics/ClassLength
         | 
| 12 8 | 
             
                    attr_accessor :issue
         | 
| 13 9 |  | 
| 14 10 | 
             
                    def initialize(issue)
         | 
| @@ -31,6 +27,7 @@ module Minitest | |
| 31 27 | 
             
                    def error_tokens
         | 
| 32 28 | 
             
                      [
         | 
| 33 29 | 
             
                        headline_tokens,
         | 
| 30 | 
            +
                        test_location_tokens,
         | 
| 34 31 | 
             
                        summary_tokens,
         | 
| 35 32 | 
             
                        *backtrace_tokens,
         | 
| 36 33 | 
             
                        newline_tokens
         | 
| @@ -40,6 +37,7 @@ module Minitest | |
| 40 37 | 
             
                    def broken_tokens
         | 
| 41 38 | 
             
                      [
         | 
| 42 39 | 
             
                        headline_tokens,
         | 
| 40 | 
            +
                        test_location_tokens,
         | 
| 43 41 | 
             
                        summary_tokens,
         | 
| 44 42 | 
             
                        *backtrace_tokens,
         | 
| 45 43 | 
             
                        newline_tokens
         | 
| @@ -49,9 +47,8 @@ module Minitest | |
| 49 47 | 
             
                    def failure_tokens
         | 
| 50 48 | 
             
                      [
         | 
| 51 49 | 
             
                        headline_tokens,
         | 
| 50 | 
            +
                        test_location_tokens,
         | 
| 52 51 | 
             
                        summary_tokens,
         | 
| 53 | 
            -
                        location_tokens,
         | 
| 54 | 
            -
                        *source_tokens,
         | 
| 55 52 | 
             
                        newline_tokens
         | 
| 56 53 | 
             
                      ]
         | 
| 57 54 | 
             
                    end
         | 
| @@ -59,7 +56,7 @@ module Minitest | |
| 59 56 | 
             
                    def skipped_tokens
         | 
| 60 57 | 
             
                      [
         | 
| 61 58 | 
             
                        headline_tokens,
         | 
| 62 | 
            -
                         | 
| 59 | 
            +
                        test_location_tokens,
         | 
| 63 60 | 
             
                        newline_tokens
         | 
| 64 61 | 
             
                      ]
         | 
| 65 62 | 
             
                    end
         | 
| @@ -67,7 +64,7 @@ module Minitest | |
| 67 64 | 
             
                    def painful_tokens
         | 
| 68 65 | 
             
                      [
         | 
| 69 66 | 
             
                        headline_tokens,
         | 
| 70 | 
            -
                         | 
| 67 | 
            +
                        slowness_summary_tokens,
         | 
| 71 68 | 
             
                        newline_tokens
         | 
| 72 69 | 
             
                      ]
         | 
| 73 70 | 
             
                    end
         | 
| @@ -75,17 +72,49 @@ module Minitest | |
| 75 72 | 
             
                    def slow_tokens
         | 
| 76 73 | 
             
                      [
         | 
| 77 74 | 
             
                        headline_tokens,
         | 
| 78 | 
            -
                         | 
| 75 | 
            +
                        slowness_summary_tokens,
         | 
| 79 76 | 
             
                        newline_tokens
         | 
| 80 77 | 
             
                      ]
         | 
| 81 78 | 
             
                    end
         | 
| 82 79 |  | 
| 83 80 | 
             
                    def headline_tokens
         | 
| 84 | 
            -
                      [[issue.type, issue | 
| 81 | 
            +
                      [[issue.type, label(issue)], spacer_token, [:default, test_name(issue)]]
         | 
| 85 82 | 
             
                    end
         | 
| 86 83 |  | 
| 87 | 
            -
                    def  | 
| 88 | 
            -
                       | 
| 84 | 
            +
                    def test_name(issue)
         | 
| 85 | 
            +
                      test_prefix = 'test_'
         | 
| 86 | 
            +
                      identifier = issue.test_identifier
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                      if identifier.start_with?(test_prefix)
         | 
| 89 | 
            +
                        identifier.delete_prefix(test_prefix).gsub('_', ' ').capitalize
         | 
| 90 | 
            +
                      else
         | 
| 91 | 
            +
                        identifier
         | 
| 92 | 
            +
                      end
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    def label(issue)
         | 
| 96 | 
            +
                      if issue.error? && issue.in_test?
         | 
| 97 | 
            +
                        # When the exception came out of the test itself, that's a different kind of exception
         | 
| 98 | 
            +
                        # that really only indicates there's a problem with the code in the test. It's kind of
         | 
| 99 | 
            +
                        # between an error and a test.
         | 
| 100 | 
            +
                        'Broken Test'
         | 
| 101 | 
            +
                      elsif issue.error?
         | 
| 102 | 
            +
                        'Error'
         | 
| 103 | 
            +
                      elsif issue.skipped?
         | 
| 104 | 
            +
                        'Skipped'
         | 
| 105 | 
            +
                      elsif issue.painful?
         | 
| 106 | 
            +
                        'Passed but Very Slow'
         | 
| 107 | 
            +
                      elsif issue.slow?
         | 
| 108 | 
            +
                        'Passed but Slow'
         | 
| 109 | 
            +
                      elsif !issue.passed?
         | 
| 110 | 
            +
                        'Failure'
         | 
| 111 | 
            +
                      else
         | 
| 112 | 
            +
                        'Success'
         | 
| 113 | 
            +
                      end
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    def test_name_and_class_tokens
         | 
| 117 | 
            +
                      [[:default, issue.test_class], *test_location_tokens]
         | 
| 89 118 | 
             
                    end
         | 
| 90 119 |  | 
| 91 120 | 
             
                    def backtrace_tokens
         | 
| @@ -94,33 +123,74 @@ module Minitest | |
| 94 123 | 
             
                      backtrace.tokens
         | 
| 95 124 | 
             
                    end
         | 
| 96 125 |  | 
| 126 | 
            +
                    def test_location_tokens
         | 
| 127 | 
            +
                      [[:default, test_file_short_location], [:muted, ':'], [:default, issue.test_definition_line], arrow_token, [:default, issue.test_failure_line], [:muted, test_line_source]]
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
             | 
| 97 130 | 
             
                    def location_tokens
         | 
| 98 | 
            -
                      [[:muted, issue. | 
| 131 | 
            +
                      [[:default, most_relevant_short_location], [:muted, ':'], [:default, issue.location.most_relevant_failure_line], [:muted, most_relevant_line_source]]
         | 
| 99 132 | 
             
                    end
         | 
| 100 133 |  | 
| 101 134 | 
             
                    def source_tokens
         | 
| 102 135 | 
             
                      filename    = issue.location.project_file
         | 
| 103 136 | 
             
                      line_number = issue.location.project_failure_line
         | 
| 104 137 |  | 
| 105 | 
            -
                       | 
| 138 | 
            +
                      source = Minitest::Heat::Source.new(filename, line_number: line_number)
         | 
| 139 | 
            +
                      [[:muted, " #{Output::SYMBOLS[:arrow]} `#{source.line.strip}`"]]
         | 
| 140 | 
            +
                    end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                    def summary_tokens
         | 
| 143 | 
            +
                      [[:italicized, issue.summary.delete_suffix('---------------').strip]]
         | 
| 144 | 
            +
                    end
         | 
| 106 145 |  | 
| 107 | 
            -
             | 
| 146 | 
            +
                    def slowness_summary_tokens
         | 
| 147 | 
            +
                      [
         | 
| 148 | 
            +
                        [:bold, slowness(issue)],
         | 
| 149 | 
            +
                        spacer_token,
         | 
| 150 | 
            +
                        [:default, issue.location.test_file.to_s.delete_prefix(Dir.pwd)],
         | 
| 151 | 
            +
                        [:muted, ':'],
         | 
| 152 | 
            +
                        [:default, issue.location.test_definition_line]
         | 
| 153 | 
            +
                      ]
         | 
| 108 154 | 
             
                    end
         | 
| 109 155 |  | 
| 110 | 
            -
                    def  | 
| 111 | 
            -
                       | 
| 156 | 
            +
                    def slowness(issue)
         | 
| 157 | 
            +
                      "#{issue.execution_time.round(2)}s"
         | 
| 112 158 | 
             
                    end
         | 
| 113 159 |  | 
| 114 160 | 
             
                    def newline_tokens
         | 
| 115 161 | 
             
                      []
         | 
| 116 162 | 
             
                    end
         | 
| 117 163 |  | 
| 118 | 
            -
                    def  | 
| 119 | 
            -
                       | 
| 164 | 
            +
                    def most_relevant_short_location
         | 
| 165 | 
            +
                      issue.location.most_relevant_file.to_s.delete_prefix("#{Dir.pwd}/")
         | 
| 166 | 
            +
                    end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                    def test_file_short_location
         | 
| 169 | 
            +
                      issue.location.test_file.to_s.delete_prefix("#{Dir.pwd}/")
         | 
| 170 | 
            +
                    end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                    def most_relevant_line_source
         | 
| 173 | 
            +
                      filename    = issue.location.project_file
         | 
| 174 | 
            +
                      line_number = issue.location.project_failure_line
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                      source = Minitest::Heat::Source.new(filename, line_number: line_number)
         | 
| 177 | 
            +
                      "\n  #{source.line.strip}"
         | 
| 178 | 
            +
                    end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                    def test_line_source
         | 
| 181 | 
            +
                      filename    = issue.location.test_file
         | 
| 182 | 
            +
                      line_number = issue.location.test_failure_line
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                      source = Minitest::Heat::Source.new(filename, line_number: line_number)
         | 
| 185 | 
            +
                      "\n  #{source.line.strip}"
         | 
| 186 | 
            +
                    end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                    def spacer_token
         | 
| 189 | 
            +
                      Output::TOKENS[:spacer]
         | 
| 120 190 | 
             
                    end
         | 
| 121 191 |  | 
| 122 | 
            -
                    def  | 
| 123 | 
            -
                       | 
| 192 | 
            +
                    def arrow_token
         | 
| 193 | 
            +
                      Output::TOKENS[:muted_arrow]
         | 
| 124 194 | 
             
                    end
         | 
| 125 195 | 
             
                  end
         | 
| 126 196 | 
             
                end
         | 
| @@ -3,23 +3,25 @@ | |
| 3 3 | 
             
            module Minitest
         | 
| 4 4 | 
             
              module Heat
         | 
| 5 5 | 
             
                class Output
         | 
| 6 | 
            +
                  # Generates the tokens to output the resulting heat map
         | 
| 6 7 | 
             
                  class Map
         | 
| 7 | 
            -
                     | 
| 8 | 
            +
                    attr_accessor :results
         | 
| 8 9 |  | 
| 9 | 
            -
                     | 
| 10 | 
            -
             | 
| 11 | 
            -
                    # def_delegators :@results, :errors, :brokens, :failures, :slows, :skips, :problems?, :slows?
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    def initialize(map)
         | 
| 14 | 
            -
                      @map = map
         | 
| 10 | 
            +
                    def initialize(results)
         | 
| 11 | 
            +
                      @results = results
         | 
| 15 12 | 
             
                      @tokens = []
         | 
| 16 13 | 
             
                    end
         | 
| 17 14 |  | 
| 18 15 | 
             
                    def tokens
         | 
| 19 | 
            -
                      map.file_hits.each do | | 
| 16 | 
            +
                      map.file_hits.each do |hit|
         | 
| 17 | 
            +
                        file_tokens = pathname(hit)
         | 
| 18 | 
            +
                        line_number_tokens = line_numbers(hit)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                        next if line_number_tokens.empty?
         | 
| 21 | 
            +
             | 
| 20 22 | 
             
                        @tokens << [
         | 
| 21 | 
            -
                          * | 
| 22 | 
            -
                          * | 
| 23 | 
            +
                          *file_tokens,
         | 
| 24 | 
            +
                          *line_number_tokens
         | 
| 23 25 | 
             
                        ]
         | 
| 24 26 | 
             
                      end
         | 
| 25 27 |  | 
| @@ -28,8 +30,24 @@ module Minitest | |
| 28 30 |  | 
| 29 31 | 
             
                    private
         | 
| 30 32 |  | 
| 33 | 
            +
                    def map
         | 
| 34 | 
            +
                      results.heat_map
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    def relevant_issue_types
         | 
| 38 | 
            +
                      # These are always relevant.
         | 
| 39 | 
            +
                      issue_types = %i[error broken failure]
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                      # These are only relevant if there aren't more serious isues.
         | 
| 42 | 
            +
                      issue_types << :skipped unless results.problems?
         | 
| 43 | 
            +
                      issue_types << :painful unless results.problems? || results.skips.any?
         | 
| 44 | 
            +
                      issue_types << :slow    unless results.problems? || results.skips.any?
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                      issue_types
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 31 49 | 
             
                    def pathname(file)
         | 
| 32 | 
            -
                      directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/"
         | 
| 50 | 
            +
                      directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/".delete_prefix('/')
         | 
| 33 51 | 
             
                      filename = file.pathname.basename.to_s
         | 
| 34 52 |  | 
| 35 53 | 
             
                      [
         | 
| @@ -40,26 +58,31 @@ module Minitest | |
| 40 58 | 
             
                    end
         | 
| 41 59 |  | 
| 42 60 | 
             
                    def hit_line_numbers(file, issue_type)
         | 
| 43 | 
            -
                      line_numbers_for_issue_type = file.issues.fetch(issue_type) { [] }
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                      return nil if line_numbers_for_issue_type.empty?
         | 
| 46 | 
            -
             | 
| 47 61 | 
             
                      numbers = []
         | 
| 48 | 
            -
                      line_numbers_for_issue_type. | 
| 62 | 
            +
                      line_numbers_for_issue_type = file.issues.fetch(issue_type) { [] }
         | 
| 63 | 
            +
                      line_numbers_for_issue_type.map do |line_number|
         | 
| 49 64 | 
             
                        numbers << [issue_type, "#{line_number} "]
         | 
| 50 65 | 
             
                      end
         | 
| 66 | 
            +
             | 
| 51 67 | 
             
                      numbers
         | 
| 52 68 | 
             
                    end
         | 
| 53 69 |  | 
| 54 70 | 
             
                    def line_numbers(file)
         | 
| 55 | 
            -
                      [
         | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
                         | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
                       | 
| 71 | 
            +
                      line_number_tokens = []
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                      # Merge the hits for all issue types into one list
         | 
| 74 | 
            +
                      relevant_issue_types.each do |issue_type|
         | 
| 75 | 
            +
                        line_number_tokens += hit_line_numbers(file, issue_type)
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                      # Sort the collected group of line number hits so they're in order
         | 
| 79 | 
            +
                      line_number_tokens.compact.sort do |a, b|
         | 
| 80 | 
            +
                        # Ensure the line numbers are integers for sorting (otherwise '100' comes before '12')
         | 
| 81 | 
            +
                        first_line_number = Integer(a[1].strip)
         | 
| 82 | 
            +
                        second_line_number = Integer(b[1].strip)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                        first_line_number <=> second_line_number
         | 
| 85 | 
            +
                      end
         | 
| 63 86 | 
             
                    end
         | 
| 64 87 | 
             
                  end
         | 
| 65 88 | 
             
                end
         |