tdd-guard-rspec 0.2.0 → 0.3.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: 7aeee4d2a9c7905a824f174feef75d1637cd26bd2f94cfac399f3268ec66741b
4
- data.tar.gz: aaaac9933f2ca6ec9670394bc430ac5ffbe2f8955734279a89a66f51c107c7d3
3
+ metadata.gz: 17d7ea075f1b90cf70693e8e26cf16008a22658102df684292403c4c7f669985
4
+ data.tar.gz: ae3a1961293344b6cfd7c80a5f9d399aa5fb48f7efd8034552c137b90188157e
5
5
  SHA512:
6
- metadata.gz: 6cd384e338d7b028ee8465b22a655579bac84978df7683763db0657a11ec1f7d1829ae1784be196ebbd9cc739324bf22bc7277b2260760cc27c44e86dc372a41
7
- data.tar.gz: 519def9baf8d5bdb0f2bb902fedf483e7fd8aa04642188bbb9b795662df030deed42ec81fdb7292d1bcfda627d808b70b49be3c2145d7f51efeb0c088f1075b4
6
+ metadata.gz: b3e389b8c466ed9dc314f14fd0fbaa8dc73f2f4a290c02b61f697953d0e393150de463538ba4d9a29dbd7a916ee603852fbd50cdb8083e3dc3a0b9c6a68cca53
7
+ data.tar.gz: b107e039edd33dd273d8e0a1b153719fb172e9c4683dc0fa5d05c4795f1dc7dc3f36e35685083190341f07b3197aa678039178e80fe64b47e61bdef26071eb6e
@@ -9,6 +9,7 @@ module TddGuardRspec
9
9
  # Mirrors the pytest reporter's single-class architecture.
10
10
  class Formatter < RSpec::Core::Formatters::BaseFormatter
11
11
  RSpec::Core::Formatters.register self,
12
+ :start,
12
13
  :example_passed,
13
14
  :example_failed,
14
15
  :example_pending,
@@ -18,22 +19,34 @@ module TddGuardRspec
18
19
 
19
20
  DEFAULT_DATA_DIR = ".claude/tdd-guard/data"
20
21
 
22
+ ANSI_ESCAPE = /\e\[[0-9;]*m/
23
+ CLASS_NAME_LINE = /\A[A-Z(][\w:() ]*:\z/
24
+
21
25
  def initialize(output)
22
26
  super(output)
23
27
  @test_results = []
24
28
  @load_errors = []
29
+ @unhandled_errors = []
25
30
  @errors_outside = 0
31
+ @expected_count = 0
26
32
  @storage_dir = determine_storage_dir
27
33
  end
28
34
 
35
+ def start(notification)
36
+ super
37
+ @expected_count = notification.count
38
+ end
39
+
29
40
  def example_passed(notification)
30
41
  record_example(notification.example, "passed")
31
42
  end
32
43
 
33
44
  def example_failed(notification)
34
45
  example = notification.example
35
- errors = [{ "message" => notification.exception.message }]
36
- record_example(example, "failed", errors)
46
+ error = { "message" => notification.exception.message }
47
+ stack = extract_relevant_stack(notification.exception.backtrace)
48
+ error["stack"] = stack if stack
49
+ record_example(example, "failed", [error])
37
50
  end
38
51
 
39
52
  def example_pending(notification)
@@ -42,7 +55,12 @@ module TddGuardRspec
42
55
 
43
56
  def message(notification)
44
57
  msg = notification.message
45
- @load_errors << msg if msg.include?("An error occurred while loading")
58
+ if msg.include?("An error occurred while loading")
59
+ @load_errors << msg
60
+ elsif msg.include?("Failure/Error:")
61
+ parsed = parse_unhandled_error(msg)
62
+ @unhandled_errors << parsed if parsed
63
+ end
46
64
  end
47
65
 
48
66
  def dump_summary(notification)
@@ -60,10 +78,18 @@ module TddGuardRspec
60
78
  end
61
79
 
62
80
  has_failures = @test_results.any? { |t| t["state"] == "failed" }
81
+ reason = if has_failures
82
+ "failed"
83
+ elsif @expected_count > 0 && @test_results.length < @expected_count
84
+ "interrupted"
85
+ else
86
+ "passed"
87
+ end
63
88
  result = {
64
89
  "testModules" => modules_map.values,
65
- "reason" => has_failures ? "failed" : "passed"
90
+ "reason" => reason
66
91
  }
92
+ result["unhandledErrors"] = @unhandled_errors unless @unhandled_errors.empty?
67
93
 
68
94
  FileUtils.mkdir_p(@storage_dir)
69
95
  File.write(File.join(@storage_dir, "test.json"), JSON.pretty_generate(result))
@@ -109,6 +135,47 @@ module TddGuardRspec
109
135
  "#{match[1]}: #{match[2].strip}"
110
136
  end
111
137
 
138
+ def extract_relevant_stack(backtrace)
139
+ return nil unless backtrace
140
+
141
+ frame = backtrace.find { |line| line.include?("spec/") && !line.include?("/gems/") }
142
+ return nil unless frame
143
+
144
+ frame.sub(%r{^.*/(?=spec/)}, "").sub(%r{^\./}, "")
145
+ end
146
+
147
+ # Parses a formatted exception string produced by RSpec's internal
148
+ # ExceptionPresenter (delivered via the :message callback for errors outside
149
+ # of examples, such as before/after :suite and :context hook failures).
150
+ #
151
+ # Returns a Hash with "name", "message", and optional "stack" on success,
152
+ # or nil if the expected structure cannot be found (safe fallback: the
153
+ # error is dropped rather than producing malformed output).
154
+ def parse_unhandled_error(raw)
155
+ lines = raw.gsub(ANSI_ESCAPE, "").split("\n")
156
+
157
+ # The header ends where the backtrace begins (first "# ..." line).
158
+ # If there is no backtrace at all, the entire message is the header.
159
+ bt_start = lines.index { |line| line.start_with?("# ") } || lines.length
160
+
161
+ header_lines = lines[0...bt_start]
162
+ class_idx = header_lines.rindex { |line| line =~ CLASS_NAME_LINE }
163
+ return nil unless class_idx
164
+
165
+ name = header_lines[class_idx].chomp(":")
166
+ message_lines = header_lines[(class_idx + 1)..-1] || []
167
+ message_body = message_lines.map { |line| line.sub(/\A /, "") }.join("\n").strip
168
+ return nil if message_body.empty?
169
+
170
+ backtrace_frames = lines[bt_start..-1].to_a.select { |line| line.start_with?("# ") }
171
+ backtrace_paths = backtrace_frames.map { |line| line.sub(/\A# /, "") }
172
+ stack = extract_relevant_stack(backtrace_paths)
173
+
174
+ result = { "name" => name, "message" => message_body }
175
+ result["stack"] = stack if stack
176
+ result
177
+ end
178
+
112
179
  def determine_storage_dir
113
180
  project_root = ENV["TDD_GUARD_PROJECT_ROOT"]
114
181
  return DEFAULT_DATA_DIR unless project_root && !project_root.empty?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tdd-guard-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.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-03 00:00:00.000000000 Z
11
+ date: 2026-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
78
  - !ruby/object:Gem::Version
79
79
  version: '0'
80
80
  requirements: []
81
- rubygems_version: 3.3.15
81
+ rubygems_version: 3.5.22
82
82
  signing_key:
83
83
  specification_version: 4
84
84
  summary: RSpec formatter for TDD Guard - enforces Test-Driven Development principles