minitest-heat 2.2.0 → 2.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de79ea86107b9d5183d3353e2c4cf6b8f5f61601080e781c611f305a4e192afd
4
- data.tar.gz: b3deddb67ef5fd2b069d2f3fe3edd754cb91df9322390dfdbe7845cab4ab73a0
3
+ metadata.gz: 5002a70b8781d74165f357718b769508a49fdb1f0de8c5321c7d2001c0e5a35c
4
+ data.tar.gz: 964154c41295fbbdf2ddd3d1111b917649d6e27ed9e741dabecab3701d81f3b6
5
5
  SHA512:
6
- metadata.gz: ce4bf462ee43dba7da46d8455caecb242f97c03a596af283c3627d50236562eca481bc6a1fbffa025b48ecfb91692c2d2a527a9a1c55603e241335256aa20b27
7
- data.tar.gz: d80070c1d120c17a4fb700980b0107b506c3046ac19463e5e89e9fa0e8ca0e3b614adde6426f1a001c6ef1492df6ce3c33af9a11d95cd63495be5eb064e26808
6
+ metadata.gz: de8d4e9468a3cd311517cb4c618d60c0b619f456df3f0f04a1184fd9c6f5aaa5bbb27b7790ad789f917d5f76e3f8dfb23bef247ec9010ced9b417f0c01476dc2
7
+ data.tar.gz: c3deb15724cf7f10656fb9ca5f2ad76079cd4c4ec3aa3a1a9eb907c246bad9197232ea43f78466456b47fa4d3414644613175390946678051f28247c92e33f69
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.2.1] - 2026-09-24
4
+
5
+ ### Fixed
6
+ - Display failures normally when a relevant source line contains non-ASCII text under a non-UTF-8 locale (such as `LC_ALL=C` or an unrecognized `LC_ALL`), or contains bytes that aren't valid UTF-8, instead of reporting that Heat couldn't display the failure. Source files are read as UTF-8 and invalid bytes are replaced, so a file that declares a non-UTF-8 encoding shows `�` for characters that aren't valid UTF-8.
7
+ - Under a non-UTF-8 locale, report failures whose messages include non-ASCII text read at runtime (such as command output or file contents) instead of losing them, and keep `--heat-json` output valid when they occur.
8
+ - Under a non-UTF-8 locale, report every failure in a project whose path contains non-ASCII characters, including errors that Minitest's own reporter can't display in that situation.
9
+
3
10
  ## [2.2.0] - 2026-09-16
4
11
 
5
12
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minitest-heat (2.2.0)
4
+ minitest-heat (2.2.1)
5
5
  minitest (>= 5.22.3, < 7)
6
6
 
7
7
  GEM
@@ -88,7 +88,7 @@ module Minitest
88
88
 
89
89
  private
90
90
 
91
- def relative_path = pathname.to_s.delete_prefix("#{Dir.pwd}/")
91
+ def relative_path = pathname.to_s.delete_prefix("#{Heat.project_root}/")
92
92
 
93
93
  def lines_summary
94
94
  line_numbers.map do |line_num|
@@ -35,7 +35,9 @@ module Minitest
35
35
  # @return [Issue] the instance of the issue to use for examining the result
36
36
  def self.from_result(result)
37
37
  # Not all results are failures, so we use the safe navigation operator
38
- exception = result.failure&.exception
38
+ failure = result.failure
39
+ unexpected = failure.is_a?(Minitest::UnexpectedError)
40
+ exception = unexpected ? failure.error : failure&.exception
39
41
 
40
42
  new(
41
43
  assertions: result.assertions,
@@ -46,11 +48,33 @@ module Minitest
46
48
  passed: result.passed?,
47
49
  error: result.error?,
48
50
  skipped: result.skipped?,
49
- message: exception&.message,
51
+ message: unexpected ? unexpected_error_message(exception) : exception&.message,
50
52
  backtrace: exception&.backtrace
51
53
  )
52
54
  end
53
55
 
56
+ # Matches the format of `Minitest::UnexpectedError#message` without calling it. Minitest
57
+ # builds that message with a binary regular expression, which raises when the project path
58
+ # and the backtrace contain non-ASCII characters under a non-UTF-8 locale.
59
+ # @param error [Exception] the exception wrapped by the unexpected error
60
+ #
61
+ # @return [String] the exception class, message, and filtered backtrace
62
+ def self.unexpected_error_message(error)
63
+ lines = (error.backtrace || []).map { |line| Heat.utf8(line) }
64
+ backtrace = Minitest.filter_backtrace(lines).map { |line| line.gsub(minitest_base_pattern, '') }
65
+
66
+ "#{error.class}: #{Heat.utf8(error.message)}\n #{backtrace.join("\n ")}"
67
+ end
68
+
69
+ # Minitest's pattern for the directory it loaded from, rebuilt as UTF-8 so it can match
70
+ # UTF-8 backtraces. Reusing it strips the same prefix as Minitest even if a test changes
71
+ # the working directory.
72
+ #
73
+ # @return [Regexp] the UTF-8 equivalent of `Minitest::UnexpectedError::BASE_RE`
74
+ def self.minitest_base_pattern
75
+ @minitest_base_pattern ||= Regexp.new(Heat.utf8(Minitest::UnexpectedError::BASE_RE.source))
76
+ end
77
+
54
78
  # Creates an instance of Issue. In general, the `from_result` approach will be more convenient
55
79
  # for standard usage, but for lower-level purposes like testing, the initializer provides3
56
80
  # more fine-grained control
@@ -67,10 +91,10 @@ module Minitest
67
91
  #
68
92
  # @return [type] [description]
69
93
  def initialize(assertions: 1, test_location: ['Unrecognized Test File', 1], backtrace: [], execution_time: 0.0, message: nil, test_class: nil, test_identifier: nil, passed: false, error: false, skipped: false)
70
- @message = message
94
+ @message = Heat.utf8(message)
71
95
 
72
96
  @assertions = Integer(assertions)
73
- @locations = Locations.new(test_location, backtrace)
97
+ @locations = Locations.new(test_location, backtrace&.map { |line| Heat.utf8(line) })
74
98
 
75
99
  @test_class = test_class
76
100
  @test_identifier = test_identifier
@@ -68,9 +68,9 @@ module Minitest
68
68
  #
69
69
  # @return [Pathname] a pathname instance for the relevant file
70
70
  def pathname
71
- Pathname(raw_pathname).expand_path
71
+ Pathname(Heat.utf8(File.expand_path(raw_pathname)))
72
72
  rescue ArgumentError
73
- Pathname(Dir.pwd)
73
+ Pathname(Heat.project_root)
74
74
  end
75
75
 
76
76
  # A safe interface to getting a string representing the path portion of the file
@@ -163,7 +163,7 @@ module Minitest
163
163
 
164
164
  private
165
165
 
166
- def project_root_dir = Dir.pwd
166
+ def project_root_dir = Heat.project_root
167
167
 
168
168
  def seconds_ago = (Time.now - mtime).to_i
169
169
  end
@@ -102,7 +102,7 @@ module Minitest
102
102
  end
103
103
 
104
104
  def file_and_line_number_tokens(location)
105
- style = location.to_s.include?(Dir.pwd) ? :bold : :muted
105
+ style = location.to_s.include?(Heat.project_root) ? :bold : :muted
106
106
  [
107
107
  [style, location.filename],
108
108
  [:muted, ':'],
@@ -118,7 +118,7 @@ module Minitest
118
118
  end
119
119
 
120
120
  def pathname(hit)
121
- directory = hit.pathname.dirname.to_s.delete_prefix("#{Dir.pwd}/")
121
+ directory = hit.pathname.dirname.to_s.delete_prefix("#{Heat.project_root}/")
122
122
  filename = hit.pathname.basename.to_s
123
123
 
124
124
  [
@@ -44,14 +44,19 @@ module Minitest
44
44
 
45
45
  # Reads (and chomps) the lines of the target file
46
46
  #
47
- # @return [type] [description]
47
+ # Ruby labels file contents with the locale's encoding, which is US-ASCII under `LC_ALL=C` or
48
+ # an unrecognized locale. Ruby source is UTF-8 by default, so lines are read as UTF-8
49
+ # regardless of locale, and bytes that aren't valid UTF-8 are replaced so that displaying
50
+ # them can't raise.
51
+ #
52
+ # @return [Array<String>] the chomped lines of the file as valid UTF-8
48
53
  def file_lines
49
- @raw_lines ||= File.readlines(filename, chomp: true)
54
+ @raw_lines ||= File.readlines(filename, chomp: true, mode: 'rb').map { |line| Heat.utf8(line) }
50
55
  # Remove trailing empty lines, checking for nil/empty safely
51
56
  @raw_lines.pop while @raw_lines.any? && @raw_lines.last&.strip.to_s.empty?
52
57
 
53
58
  @raw_lines
54
- rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR, IOError, Encoding::UndefinedConversionError
59
+ rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR, IOError
55
60
  # Occasionally, for a variety of reasons, a file can't be read. In those cases, it's best to
56
61
  # return no source code lines rather than have the test suite raise an error unrelated to
57
62
  # the code being tested because that gets confusing.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
- VERSION = '2.2.0'
5
+ VERSION = '2.2.1'
6
6
  end
7
7
  end
data/lib/minitest/heat.rb CHANGED
@@ -48,5 +48,30 @@ module Minitest
48
48
  def self.configure
49
49
  yield(configuration)
50
50
  end
51
+
52
+ # Ruby labels text from outside Ruby source (paths, file contents, command output) with the
53
+ # locale's encoding, which is US-ASCII or binary under `LC_ALL=C` or an unrecognized locale.
54
+ # That text is overwhelmingly UTF-8, so unlabeled bytes are read as UTF-8 and text in any other
55
+ # encoding is converted. Anything that isn't valid UTF-8 is replaced so that comparing,
56
+ # displaying or serializing the text can't raise.
57
+ #
58
+ # @param text [String, nil] text from outside Ruby source
59
+ #
60
+ # @return [String, nil] valid UTF-8 text
61
+ def self.utf8(text)
62
+ return text unless text.is_a?(String)
63
+
64
+ case text.encoding
65
+ when Encoding::UTF_8, Encoding::US_ASCII, Encoding::BINARY
66
+ String.new(text, encoding: Encoding::UTF_8).scrub
67
+ else
68
+ text.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
69
+ end
70
+ end
71
+
72
+ # The directory the tests run from, which identifies project files
73
+ #
74
+ # @return [String] the current working directory as UTF-8
75
+ def self.project_root = utf8(Dir.pwd)
51
76
  end
52
77
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minitest-heat
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Garrett Dimon