tdd-guard-minitest 0.1.0 → 0.2.0

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: 7094ecd33f1d7f2ea05de6193fe1ad6bf7bb38bc8e990b6670c4844e0df29892
4
- data.tar.gz: d7fd5b92ee9ed72fd5f82efe2ba7496637389e0080f4b1e279a8963ebdc6fa53
3
+ metadata.gz: f78fce6dfb331408b3524d8c8ae2d727829a4dead71b875433a4f68b0011d758
4
+ data.tar.gz: 1b86798bd82f35a31a5601fabb6f10f8a47cf2be70a5a0d5c5e7db7e37ba5e62
5
5
  SHA512:
6
- metadata.gz: 8063e1b3dca4a7a27caaddb1d2ee3078b8216847097772348f0f607e95d4985eb521d8352e8efdad6055e789429b6e570f589a3d411dd90402f10c774d39bef7
7
- data.tar.gz: 4852220c5f486143695c117b342f7161572af18babfdeedd8b5be341134473edd015c914232e4f0c4681e9070cc56ef48fa17ef8a9da7b112608c7f93cbebe66
6
+ metadata.gz: f19357d0e0ae8cac4f0b0db8c607905727e0dfb36c72fa7f387f69e9da3c568fe16466174f792669dd15ef29dcbe804b947fe6d1d5c5ab9634a7cb9b589c2554
7
+ data.tar.gz: c10546e57c08e637fa6d304af8b10ec8a49f97df610311b274f3d26807086a544d034f2feef18b3e6fdd093939f1594f314d6d4b83c1be9ad0a902d5cea684d5
@@ -6,9 +6,11 @@ require "minitest"
6
6
 
7
7
  module TddGuardMinitest
8
8
  @unhandled_errors = []
9
+ @reported = false
9
10
 
10
11
  class << self
11
12
  attr_reader :unhandled_errors
13
+ attr_accessor :reported
12
14
  end
13
15
 
14
16
  # Minitest reporter that captures test results for TDD Guard validation.
@@ -34,10 +36,17 @@ module TddGuardMinitest
34
36
  # point's at_exit hook when $! is set.
35
37
  #
36
38
  # Injects a synthetic entry into @test_results and writes the JSON
37
- # through the normal report path. Skips if test.json already exists
38
- # to avoid clobbering real results.
39
+ # through the normal report path. Skips when this process has already
40
+ # written test.json via the normal Minitest flow, so it never clobbers
41
+ # real results from the same run. A stale test.json left behind by a
42
+ # previous process is overwritten so the file always reflects the most
43
+ # recent run's state.
39
44
  def self.handle_load_error(exception)
40
45
  new(StringIO.new).handle_load_error(exception)
46
+ rescue ArgumentError
47
+ # Project root is not configured; the user has already seen the
48
+ # configuration error from the main test run. Avoid double-raising
49
+ # from the autorun at_exit hook.
41
50
  end
42
51
 
43
52
  # Reads the existing test.json, merges in the unhandledErrors field,
@@ -45,6 +54,8 @@ module TddGuardMinitest
45
54
  # autorun.rb after Minitest.after_run blocks have completed.
46
55
  def self.append_unhandled_errors(errors)
47
56
  new(StringIO.new).append_unhandled_errors(errors)
57
+ rescue ArgumentError
58
+ # Same as above: skip when the project root is not configured.
48
59
  end
49
60
 
50
61
  def append_unhandled_errors(errors)
@@ -57,7 +68,7 @@ module TddGuardMinitest
57
68
  end
58
69
 
59
70
  def handle_load_error(exception)
60
- return if File.exist?(File.join(@storage_dir, "test.json"))
71
+ return if TddGuardMinitest.reported
61
72
 
62
73
  add_load_error(exception)
63
74
  report
@@ -109,6 +120,7 @@ module TddGuardMinitest
109
120
 
110
121
  FileUtils.mkdir_p(@storage_dir)
111
122
  File.write(File.join(@storage_dir, "test.json"), JSON.pretty_generate(result))
123
+ TddGuardMinitest.reported = true
112
124
  end
113
125
 
114
126
  def passed?
@@ -119,6 +131,17 @@ module TddGuardMinitest
119
131
 
120
132
  def compute_expected_count
121
133
  filter = options[:filter]
134
+ # Skip the count when the filter cannot be reliably matched against
135
+ # method names. Two cases motivate this:
136
+ # - filter is something other than String/Regexp (e.g. a Proc), which
137
+ # Minitest's grep-based methods_matching cannot count.
138
+ # - Rails passes line-targeted runs (`rails test path:N`) via
139
+ # options[:test_files] as "path:N" entries while leaving filter nil,
140
+ # so runnable_methods returns the file's full set and an inflated
141
+ # expected_count would falsely flip the run's reason to "interrupted".
142
+ return 0 if filter && !filter.is_a?(String) && !filter.is_a?(Regexp)
143
+ return 0 if line_targeted?(options[:test_files])
144
+
122
145
  Minitest::Runnable.runnables.sum do |klass|
123
146
  if filter
124
147
  klass.methods_matching(filter).size
@@ -128,9 +151,14 @@ module TddGuardMinitest
128
151
  end
129
152
  end
130
153
 
154
+ def line_targeted?(test_files)
155
+ return false unless test_files.is_a?(Array)
156
+ test_files.any? { |entry| entry.is_a?(String) && entry =~ /:\d+\z/ }
157
+ end
158
+
131
159
  def build_unhandled_error(exception)
132
160
  name = exception.class.name || "(anonymous error class)"
133
- error = { "name" => name, "message" => exception.message }
161
+ error = { "name" => name, "message" => scrub_utf8(exception.message) }
134
162
  stack = extract_relevant_stack(exception.backtrace)
135
163
  error["stack"] = stack if stack
136
164
  error
@@ -139,16 +167,31 @@ module TddGuardMinitest
139
167
  def build_error(failure)
140
168
  if failure.is_a?(Minitest::UnexpectedError)
141
169
  exception = failure.error
142
- error = { "message" => exception.message }
170
+ error = { "message" => scrub_utf8(exception.message) }
143
171
  stack = extract_relevant_stack(exception.backtrace)
144
172
  else
145
- error = { "message" => failure.message }
173
+ error = { "message" => scrub_utf8(failure.message) }
146
174
  stack = extract_relevant_stack(failure.backtrace)
147
175
  end
148
176
  error["stack"] = stack if stack
149
177
  error
150
178
  end
151
179
 
180
+ # Replace bytes that cannot be represented as UTF-8 so that
181
+ # JSON.pretty_generate does not raise on binary or alternately
182
+ # encoded strings (e.g. Shift_JIS, ASCII-8BIT). Valid UTF-8
183
+ # strings, including Japanese, pass through unchanged.
184
+ def scrub_utf8(str)
185
+ return str unless str.is_a?(String)
186
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
187
+
188
+ if str.encoding == Encoding::UTF_8
189
+ str.scrub
190
+ else
191
+ str.encode("UTF-8", invalid: :replace, undef: :replace)
192
+ end
193
+ end
194
+
152
195
  def extract_relevant_stack(backtrace)
153
196
  return nil unless backtrace
154
197
 
@@ -164,8 +207,17 @@ module TddGuardMinitest
164
207
  source = result.source_location
165
208
  return "unknown" unless source
166
209
 
167
- path = source.first
168
- # Convert absolute path to relative path from cwd
210
+ relative_path(source.first)
211
+ end
212
+
213
+ # Strip a leading cwd prefix and any "./" so file paths in test.json
214
+ # are reported relative to the project root regardless of whether they
215
+ # arrived as absolute paths (from a backtrace) or already-relative
216
+ # paths (from result.source_location).
217
+ def relative_path(path)
218
+ return "unknown" if path.nil? || path.to_s.empty?
219
+
220
+ path = path.to_s
169
221
  cwd = "#{Dir.pwd}/"
170
222
  path = path.delete_prefix(cwd) if path.start_with?(cwd)
171
223
  path.sub(%r{^\./}, "")
@@ -173,30 +225,47 @@ module TddGuardMinitest
173
225
 
174
226
  def determine_storage_dir
175
227
  project_root = ENV["TDD_GUARD_PROJECT_ROOT"]
176
- return DEFAULT_DATA_DIR unless project_root && !project_root.empty?
177
- return DEFAULT_DATA_DIR unless absolute_path?(project_root)
178
- return DEFAULT_DATA_DIR unless cwd_within?(project_root)
228
+ if project_root.nil? || project_root.empty?
229
+ raise ArgumentError,
230
+ "project root must be configured via TDD_GUARD_PROJECT_ROOT environment variable"
231
+ end
179
232
 
180
- File.join(project_root, DEFAULT_DATA_DIR)
181
- end
233
+ expanded = File.expand_path(project_root)
234
+ unless File.directory?(expanded)
235
+ raise ArgumentError,
236
+ "project root does not exist: #{expanded.inspect}"
237
+ end
182
238
 
183
- def absolute_path?(path)
184
- File.absolute_path?(path)
239
+ resolved = canonical_path(expanded)
240
+ unless cwd_within?(resolved)
241
+ raise ArgumentError,
242
+ "current directory must be within project root #{resolved.inspect}"
243
+ end
244
+
245
+ File.join(resolved, DEFAULT_DATA_DIR)
185
246
  end
186
247
 
187
248
  def cwd_within?(root)
188
- expanded = File.expand_path(root)
189
- cwd = Dir.pwd
190
- cwd == expanded || cwd.start_with?("#{expanded}/")
249
+ cwd = canonical_path(Dir.pwd)
250
+ cwd == root || cwd.start_with?("#{root}/")
251
+ end
252
+
253
+ # Resolve symlinks when the path exists so that platforms with
254
+ # symlinked tempdirs (macOS /var -> /private/var) compare consistently.
255
+ def canonical_path(path)
256
+ File.realpath(path)
257
+ rescue Errno::ENOENT
258
+ path
191
259
  end
192
260
 
193
261
  # Injects a synthetic failed test entry derived from an exception raised
194
262
  # before Minitest could run.
195
263
  def add_load_error(exception)
196
264
  frame = first_user_frame(exception.backtrace)
197
- file_path = frame ? frame.split(":", 2).first.to_s.sub(%r{^\./}, "") : "unknown"
198
- name = "#{exception.class}: #{exception.message.lines.first.to_s.strip}"
199
- message = build_load_error_message(exception, frame)
265
+ file_path = frame ? relative_path(frame.split(":", 2).first) : "unknown"
266
+ msg = scrub_utf8(exception.message)
267
+ name = "#{exception.class}: #{msg.lines.first.to_s.strip}"
268
+ message = build_load_error_message(exception, frame, msg)
200
269
 
201
270
  @test_results << {
202
271
  "name" => name,
@@ -214,11 +283,12 @@ module TddGuardMinitest
214
283
  end
215
284
  end
216
285
 
217
- def build_load_error_message(exception, frame)
218
- header = "#{exception.class}: #{exception.message}"
286
+ def build_load_error_message(exception, frame, message = nil)
287
+ msg = message || scrub_utf8(exception.message)
288
+ header = "#{exception.class}: #{msg}"
219
289
  return header unless frame
220
290
 
221
- "#{header}\n #{frame.sub(%r{^\./}, '')}"
291
+ "#{header}\n #{relative_path(frame)}"
222
292
  end
223
293
  end
224
294
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tdd-guard-minitest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiro-Chiba
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
11
+ date: 2026-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest