rspec-debugging 0.0.4 → 0.0.6

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: 18dc39f0ec372fefa54e665b0f8fa941ee58d2e5437d0f7baf165411a7ae3706
4
- data.tar.gz: bd59263041e3197220b04c1cda058e1d57fe91eb86ed4a50d461c06c02620c98
3
+ metadata.gz: cef0952745e075d2ddb60bf6827985ae51dcc935b14624f97bebb48bda5b29f1
4
+ data.tar.gz: 104a0eae0841e4c02dc15004254f020c87f7f79aaa4058183c88a1d3bbe2ef54
5
5
  SHA512:
6
- metadata.gz: f1a1432398be62a3f54c4c3b198b94b6a69ac9f1524a36f25026ff53bfd5af7ec17dcb0b64c2f01f6b3886492055121f961216f5a818cd37ca2bfbf831fed4cc
7
- data.tar.gz: eda816fa9f998da2aaf4075010af9e06bafca4b2c308609a2285e0fc202c325284e77eefa5d39ddca787f4a18f7b4ada05265955c08caa2bdd5bc14969765f90
6
+ metadata.gz: f4d818d570b7d1c2d1ddfaf919112b2291a996b6dc2e064b18b342e8dc997c8dcf77603bcb961c0a2c45b21a4c49949a9f0bb6afb14ce6e7495113b0905a6fbd
7
+ data.tar.gz: affde56467e8d7ae4c842a9cd83cff7bd8833f4c09a7acdfb276b6e8e963ec72e6dc3322c431349e1d80e4b3e1d65041fed38ad7d635e46f19ae02f210cd9a11
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ ### 0.0.6 (2026-11-03)
2
+ - fix: an example that closes `$stdin` and then fails skips the postmortem debugger instead of crashing its thread with `IOError: closed stream`
3
+
4
+ ### 0.0.5 (2026-11-03)
5
+ - feature: failures in `before` / `after` hooks and `let!` now open the postmortem debugger
6
+ - feature: the postmortem banner lists the names of initialized `let` variables
7
+ - fix: quitting the debugger no longer ends the run as a pass; the failing example is reported and the run stops with a non-zero exit
8
+ - fix: `dit` never opens the debugger when stdin isn't a TTY (CI), so it can't block a run
9
+ - fix: `dit` blocks that take the example (`dit "x" do |example|`) receive it again instead of nil
10
+ - fix: examples inside `ddescribe` / `dcontext` now open the debugger
11
+ - fix: `skip`, `pending` and `exit` no longer open the debugger; only real failures do
12
+ - fix: blockless `dit` reports the right location on Ruby 4.0, and blockless `it` / `dit ... do` examples no longer report a location inside rspec-debugging
13
+ - fix: declare `debug` and `rspec-core` as runtime dependencies; the gem failed to load (`LoadError: cannot load such file -- debug`) in a bundle without `debug`
14
+ - drop Ruby 3.1 (end of life); requires Ruby >= 3.2
15
+ - fix: `let_variable_values` no longer memoizes the lets its definitions reference, so inspecting a let doesn't change what the example does next
16
+ - fix: `let_variable_values` returns a raising definition's error as that scope's value instead of aborting the whole call
17
+ - fix: let helpers accept String names, and raise `ArgumentError` naming an unknown let instead of `undefined method 'map' for nil` (or quietly returning nil/false)
18
+ - fix: `let_variable_*(:subject)` follows a named subject (`subject(:thing)`) to its let instead of crashing or returning an outer group's subject
19
+ - fix: the gem's private helpers (`normalize_path`, `let_variable_methods`) are no longer mixed into example groups, where they collided with user helpers of the same name
20
+ - fix: let helpers work inside `before(:context)` (lets read as uninitialized) instead of raising `undefined method 'key?' for nil`
21
+ - fix: `let_variable_values` reports the right value for a definition that calls `super()` after the let has been memoized (it used to build on the memoized value)
22
+ - fix: `let_variable_values` passes the current example to `let(:x) { |example| ... }` definitions instead of reporting an `ArgumentError`
23
+ - fix: `dit` no longer opens the debugger when stderr isn't a TTY (e.g. `rspec --bisect` children, which showed a bare debugger prompt)
24
+ - fix: `--dry-run` reports a blockless `dit` as pending, not passed
25
+ - fix: let helpers no longer treat a described class whose name contains `LetDefinitions` as a source of let variables
26
+ - fix: `dit` no longer opens the debugger when stdout is redirected (e.g. `rspec -f json > out.json`), which looked like a hang and corrupted the output
27
+ - fix: the postmortem no longer hides your own code under `lib/rspec/` or in a project checked out under `lib/rspec-*/`
28
+ - fix: a blockless `dit` under `xdescribe`, a skipped group, or `pending` (on the group or itself) stays pending instead of becoming a breakpoint
29
+ - fix: debugged examples no longer tag exceptions with debugger state, so rescued errors and failures can be marshaled again, and raising a frozen exception no longer crashes with "exception reentered"
30
+ - fix: rspec-debugging's own frames no longer appear in failure backtraces when the gem is loaded from a path or git checkout
31
+
1
32
  ### 0.0.4 (2025-02-08)
2
33
  - dependency fix
3
34
  - readme
data/Gemfile.lock CHANGED
@@ -1,54 +1,66 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-debugging (0.0.4)
5
- rspec-expectations (>= 3)
4
+ rspec-debugging (0.0.5)
5
+ debug (>= 1.10)
6
+ rspec-core (>= 3.10)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
- debug (1.9.2)
11
+ cgi (0.5.1)
12
+ date (3.5.1)
13
+ debug (1.11.1)
11
14
  irb (~> 1.10)
12
15
  reline (>= 0.3.8)
13
- diff-lcs (1.5.1)
16
+ diff-lcs (1.6.2)
14
17
  docile (1.4.1)
15
- io-console (0.7.2)
16
- irb (1.14.1)
18
+ erb (4.0.4.1)
19
+ cgi (>= 0.3.3)
20
+ io-console (0.8.2)
21
+ irb (1.16.0)
22
+ pp (>= 0.6.0)
17
23
  rdoc (>= 4.0.0)
18
24
  reline (>= 0.4.2)
19
- psych (5.2.0)
25
+ pp (0.6.3)
26
+ prettyprint
27
+ prettyprint (0.2.0)
28
+ psych (5.3.1)
29
+ date
20
30
  stringio
21
- rdoc (6.8.1)
31
+ rdoc (7.0.1)
32
+ erb
22
33
  psych (>= 4.0.0)
23
- reline (0.5.11)
34
+ tsort
35
+ reline (0.6.3)
24
36
  io-console (~> 0.5)
25
- rspec (3.13.0)
37
+ rspec (3.13.2)
26
38
  rspec-core (~> 3.13.0)
27
39
  rspec-expectations (~> 3.13.0)
28
40
  rspec-mocks (~> 3.13.0)
29
- rspec-core (3.13.2)
41
+ rspec-core (3.13.6)
30
42
  rspec-support (~> 3.13.0)
31
- rspec-expectations (3.13.3)
43
+ rspec-expectations (3.13.5)
32
44
  diff-lcs (>= 1.2.0, < 2.0)
33
45
  rspec-support (~> 3.13.0)
34
- rspec-mocks (3.13.2)
46
+ rspec-mocks (3.13.6)
35
47
  diff-lcs (>= 1.2.0, < 2.0)
36
48
  rspec-support (~> 3.13.0)
37
- rspec-support (3.13.1)
49
+ rspec-support (3.13.6)
38
50
  simplecov (0.22.0)
39
51
  docile (~> 1.1)
40
52
  simplecov-html (~> 0.11)
41
53
  simplecov_json_formatter (~> 0.1)
42
54
  simplecov-html (0.13.1)
43
55
  simplecov_json_formatter (0.1.4)
44
- stringio (3.1.2)
56
+ stringio (3.2.0)
57
+ tsort (0.2.0)
45
58
 
46
59
  PLATFORMS
47
60
  arm64-darwin-24
48
61
  ruby
49
62
 
50
63
  DEPENDENCIES
51
- debug
52
64
  rspec
53
65
  rspec-debugging!
54
66
  simplecov
data/README.md CHANGED
@@ -6,86 +6,161 @@ rspec-debugging
6
6
 
7
7
  Tools to make debugging RSpec tests easier.
8
8
 
9
+ ```ruby
10
+ # Gemfile
11
+ gem "rspec-debugging"
12
+
13
+ # spec/spec_helper.rb
14
+ require "rspec/debugging"
15
+ ```
16
+
17
+ Built on the [debug](https://github.com/ruby/debug) gem.
18
+
9
19
  <br />
10
20
 
11
21
  Debug broken specs
12
22
  ----
13
- Use `dit`, `dcontext`, or `ddescribe` to run a subset of examples and and start the debugger if anything breaks.
23
+ Swap `it` for `dit` (or `describe` / `context` for `ddescribe` / `dcontext`). When the example fails, you land in the debugger right where it broke, with the stack intact.
14
24
 
15
25
  ```ruby
16
- require "rspec/debugging"
17
-
18
26
  describe MySpec do
19
27
  dit { is_expected.to ... }
20
28
  end
21
29
  ```
22
30
 
23
- or set an environment variable to run and debug all specs:
31
+ Failures in `before` / `after` hooks and `let!` count too. Before the prompt opens, you get the error and the names of any `let` variables that were initialized:
32
+
33
+ ```
34
+ Error:
35
+
36
+ expected: "Bob"
37
+ got: "Ada"
38
+
39
+ (compared using ==)
40
+
41
+
42
+ initialized lets: user
43
+ ```
44
+
45
+ Getting around: `c` (continue) re-raises the error, so the example fails as usual and the run carries on. `q` quits, which stops the run and fails it; it asks `Really quit? [Y/n]` first, and `q!` skips the question.
46
+
47
+ Or debug every example, no code changes:
24
48
  ```
25
49
  RSPEC_DEBUGGING=true bundle exec rspec
26
50
  ```
27
51
 
52
+ Tip: `RSPEC_DEBUGGING=true bundle exec rspec --only-failures --fail-fast` debugs the first remaining failure. `--only-failures` needs one prior run and this line in your config:
53
+
54
+ ```ruby
55
+ config.example_status_persistence_file_path = "spec/examples.txt"
56
+ ```
57
+
58
+ Use `rspec path/to/spec.rb:42` to target one example.
59
+
28
60
  <br />
29
61
 
30
62
  Stop and explore
31
63
  ----
32
- Drop `dit` anywhere to start a debugger.
64
+ A `dit` with no block is a breakpoint.
33
65
 
34
66
  ```ruby
35
67
  describe MySpec do
68
+ let(:user) { ... }
69
+
36
70
  dit
37
71
  end
38
72
  ```
39
73
 
40
74
  <br />
41
75
 
76
+ Good to know
77
+ ----
78
+ * `dit` and friends tag examples `focus: true`, so they only narrow the run if your config has `config.filter_run_when_matching :focus`.
79
+ * Keep a stray `dit` from being committed: [rubocop-rspec](https://github.com/rubocop/rubocop-rspec)'s `RSpec/Focus` cop flags them once they're in its list of focused methods (`fit` and friends stay flagged):
80
+
81
+ ```yaml
82
+ # .rubocop.yml
83
+ RSpec:
84
+ Language:
85
+ ExampleGroups:
86
+ Focused: [ddescribe, dcontext]
87
+ Examples:
88
+ Focused: [dit]
89
+ ```
90
+ * The debugger only opens when stdin, stdout and stderr are all terminals. Redirect any of them (CI, or `rspec -f json > out.json`) and nothing interactive happens, so a stray `dit` won't hang CI. It still tags `focus: true`, though, so like a stray `fit` it can narrow the run to that one example. A blockless `dit` reports as pending ("Not yet implemented").
91
+ * A `Timeout`-style `around` hook (`Timeout.timeout(n) { ex.run }`) keeps ticking while you debug. When it expires, it breaks the debugger session and the example fails with the timeout instead of the original error.
92
+ * `skip` and `pending` don't open the debugger; only real failures do.
93
+ * Not covered: `before(:all)` failures, unmet mock expectations, and `aggregate_failures`.
94
+ * While debugging, every raised exception has its frames captured — even ones your code rescues — so `RSPEC_DEBUGGING=true` can slow down exception-heavy suites.
95
+
96
+ <br />
97
+
42
98
  Got let variables?
43
99
  ----
44
- `let` variables are great, but complexity can obscure their value.
100
+ `let` variables are great, but complexity can obscure their values.
45
101
 
46
102
  ```ruby
47
- describe RSpec::Debugging do
48
- describe RSpec::Debugging::LetVariables do
49
- subject { a }
103
+ describe "a" do
104
+ subject { a }
50
105
 
51
- let(:a) { "a" }
52
- let!(:b) { "b" }
106
+ let(:a) { "a" }
107
+ let!(:b) { "b" }
53
108
 
54
- it { is_expected.to be ??? }
109
+ it { is_expected.to eq ??? }
55
110
 
56
- context "when we nest" do
57
- let(:a) { "A" + b }
111
+ context "when we nest" do
112
+ let(:a) { "A" + b }
58
113
 
59
- it { is_expected.to be ??? }
114
+ it { is_expected.to eq ??? }
60
115
 
61
- context "...and nest" do
62
- let(:a) { super() * 2 }
116
+ context "...and nest" do
117
+ let(:a) { super() * 2 }
63
118
 
64
- it { is_expected.to be ??? }
65
- end
119
+ it { is_expected.to eq ??? }
66
120
  end
121
+ end
122
+ end
67
123
  ```
68
124
 
69
- ...add a hundred lines of legacy code and then solve for `a`. Or use tooling :)
125
+ ...add a hundred lines of legacy code and then solve for `a`. Or use tooling :)
70
126
 
71
- <br />
127
+ ```ruby
128
+ # inside the innermost example
129
+ let_variables # => [:a, :b, :subject]
130
+ let_variable_initialized?(:a) # => false
131
+ let_variable_initialized?(:b) # => true, thanks to let!
132
+ let_variable_locations(:a) # => ["spec/a_spec.rb:15", "spec/a_spec.rb:10", "spec/a_spec.rb:4"]
133
+ let_variable_values(:a)
134
+ # => [
135
+ # { "spec/a_spec.rb:15" => "AbAb" },
136
+ # { "spec/a_spec.rb:10" => "Ab" },
137
+ # { "spec/a_spec.rb:4" => "a" },
138
+ # ]
139
+ ```
72
140
 
73
- ----
141
+ Names can be a Symbol or a String. An unknown name raises `ArgumentError`.
74
142
 
75
143
  ### let_variables
76
- * list of variables defined by RSpec `let`
77
-
144
+ * names of all `let` variables in scope (including `subject`)
78
145
 
79
146
  ### let_variable_initialized?(name)
80
- * whether a specified let variable been initialized
81
-
147
+ * whether the variable has been evaluated yet in this example
82
148
 
83
149
  ### let_variable_get(name)
84
- * the value of a let variable, if initialized
150
+ * the memoized value, or `nil` if it hasn't been evaluated. Never triggers evaluation.
151
+
152
+ ### let_variable_reset(name)
153
+ * forget the memoized value, so the next call re-evaluates it
154
+
155
+ ### let_variable_locations(name)
156
+ * every place the variable is defined, innermost first
85
157
 
86
158
  ### let_variable_values(name)
87
- * all locations where the specified variable has been defined or redefined, and it's value at each location
159
+ * every place the variable is defined, innermost first, with the value each definition produces
160
+ * doesn't memoize anything, so the example carries on as if you never looked. If a definition raises, its error is its value.
161
+ * it does run each definition, though, so external side effects (DB writes, HTTP calls) still happen
88
162
 
163
+ The helpers only see `let` definitions: a plain `def` that overrides a `let` of the same name isn't detected.
89
164
 
90
165
  <br />
91
166
 
@@ -6,55 +6,149 @@ module RSpec
6
6
  module DebugIt
7
7
  ENABLED = ['1', 'true'].include?(ENV['RSPEC_DEBUGGING'])
8
8
 
9
- def initialize(example_group_class, description, user_metadata, example_block=nil)
10
- return super unless user_metadata[:debug] || ENABLED
9
+ GEM_LIB = %r{\A#{Regexp.escape(File.expand_path("../..", __dir__))}/}
11
10
 
12
- if example_block
13
- orig_example_block = example_block
11
+ # Frames to hide from the postmortem: rspec's gems and executable, and this
12
+ # gem. Anchored to those dirs so user code under lib/rspec/ stays visible.
13
+ HIDDEN_FRAMES = Regexp.union(
14
+ %r{/rspec-(core|expectations|mocks|support)(-[^/]+)?/lib/},
15
+ %r{/(exe|bin)/rspec\z},
16
+ GEM_LIB,
17
+ %r{\A<internal:trace_point>\z},
18
+ )
14
19
 
15
- example_block = Proc.new do
16
- e = DEBUGGER__::SESSION.capture_exception_frames /(exe|bin|lib)\/rspec/ do
17
- instance_exec(&orig_example_block)
18
- end
20
+ # Mirrors debug's SESSION.capture_exception_frames, which tags every
21
+ # exception raised on any thread while it runs, rescued and frozen ones
22
+ # included. This watches only this thread and keeps the frames in a local
23
+ # table, keyed weakly so rescued exceptions can still be collected (not
24
+ # WeakMap: it holds values weakly too, so a GC would drop the frames).
25
+ # Ruby 3.2 lacks WeakKeyMap. Returns [error, frames] for an error that
26
+ # escapes the block.
27
+ def self.capture_frames
28
+ frames = defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new : {}.compare_by_identity
29
+ hook = TracePoint.new(:raise) do |tp|
30
+ # C frames have a nil path on Ruby 4.0
31
+ frames[tp.raised_exception] = DEBUGGER__.capture_frames(nil).reject { |f| f.path&.match?(HIDDEN_FRAMES) }
32
+ rescue StandardError
33
+ # WeakKeyMap calls the exception's #hash; if that raises, skip it
34
+ end
19
35
 
20
- if e
21
- $stderr.puts <<~MSG
36
+ hook.enable(target_thread: Thread.current) { yield }
37
+ rescue Exception => error
38
+ found = (frames[error] rescue nil)
39
+ found ? [error, found] : raise
40
+ end
22
41
 
23
- Error:
24
- #{e.message}
42
+ # Skip, an expected failure of a pending example, and exit aren't failures.
43
+ # Nor is anything once the run is stopping.
44
+ def self.failure?(error)
45
+ !RSpec.world.wants_to_quit &&
46
+ !RSpec.current_example.pending? &&
47
+ !error.is_a?(RSpec::Core::Pending::SkipDeclaredInExample) &&
48
+ RSpec::Support::AllExceptionsExceptOnesWeMustNotRescue === error
49
+ end
25
50
 
26
- MSG
51
+ # Quitting the debugger raises SystemExit, which would end the run as a
52
+ # pass. Stop the run instead; the examples it skips make it fail.
53
+ def self.session
54
+ yield
55
+ rescue SystemExit
56
+ RSpec.world.wants_to_quit = true
57
+ end
27
58
 
28
- DEBUGGER__::SESSION.enter_postmortem_session e
29
- raise e
30
- end
31
- end # Proc.new
32
- elsif user_metadata[:debug] && user_metadata[:skip] == RSpec::Core::Pending::NOT_YET_IMPLEMENTED
33
- # called with no block
34
- user_metadata.delete(:skip)
59
+ # The console reads stdin and writes stdout; our banner goes to stderr.
60
+ # Any one redirected (rspec -f json > out, --bisect children) or closed
61
+ # (tty? raises on a closed IO) means no one can drive it.
62
+ def self.interactive?
63
+ [$stdin, $stdout, $stderr].all? { |io| !io.closed? && io.tty? } &&
64
+ !RSpec.configuration.dry_run?
65
+ end
35
66
 
36
- location = RSpec::Core::Metadata.relative_path(caller[2].split(":in").first)
67
+ # Hooks in at run time, not in initialize: an extra frame there would
68
+ # become the location RSpec records for blockless examples.
69
+ def run(example_group_instance, reporter)
70
+ if (ENABLED || metadata[:debug]) && DebugIt.interactive?
71
+ debug_blockless if breakpoint?
72
+ example_group_instance.extend(Postmortem)
73
+ end
37
74
 
38
- example_block = Proc.new do
39
- $stderr.puts <<~MSG
40
- debugging: #{location}
75
+ super
76
+ end
41
77
 
42
- MSG
78
+ private
43
79
 
44
- debugger
45
- end.tap do |block|
46
- # redefine source_location to return the correct location
47
- block.define_singleton_method(:source_location) { location.split(":") }
48
- end
80
+ # A blockless dit, not otherwise skipped or pending. Its "Not yet
81
+ # implemented" skip overrides one inherited from the group, so ask the group.
82
+ def breakpoint?
83
+ metadata[:dit] &&
84
+ metadata[:skip] == RSpec::Core::Pending::NOT_YET_IMPLEMENTED &&
85
+ !metadata[:pending] &&
86
+ !example_group.metadata[:skip]
87
+ end
88
+
89
+ # A blockless dit opens the debugger instead of being "Not yet implemented".
90
+ def debug_blockless
91
+ metadata.delete(:skip)
92
+ location = self.location
93
+
94
+ @example_block = Proc.new do
95
+ $stderr.puts <<~MSG
96
+ debugging: #{location}
97
+
98
+ MSG
99
+
100
+ DebugIt.session { debugger }
49
101
  end
102
+ end
50
103
 
51
- super
104
+ # Extended onto the example group instance a debugged example runs in.
105
+ module Postmortem
106
+ # rspec-core runs the example body and its before/after hooks (so let!
107
+ # too) as instance_exec(example, &block). Around hooks get a Procsy, and
108
+ # user code passes other args, so a failure is only debugged once. In a
109
+ # thread there is no current example: leave that alone too.
110
+ def instance_exec(*args, &block)
111
+ example = RSpec.current_example
112
+ return super unless example && args.first.equal?(example)
113
+
114
+ error, frames = DebugIt.capture_frames { return super }
115
+ # the example may have closed or swapped stdin since run checked
116
+ raise error unless DebugIt.failure?(error) && DebugIt.interactive?
117
+
118
+ $stderr.puts <<~MSG
119
+
120
+ Error:
121
+ #{error.message}
122
+
123
+ MSG
124
+
125
+ # names only: values can be huge
126
+ lets = let_variables.select { |name| let_variable_initialized?(name) }
127
+ $stderr.puts "initialized lets: #{lets.join(", ")}\n\n" if lets.any?
128
+
129
+ # debug reads the frames from this ivar. Set it on a copy so the
130
+ # user's error stays as raised (it may be frozen, or get marshaled).
131
+ # (A copy that can't be made just skips the session.)
132
+ copy = begin
133
+ error.dup
134
+ rescue Exception
135
+ nil
136
+ end
137
+ if copy
138
+ copy.instance_variable_set(:@__debugger_postmortem_frames, frames)
139
+ DebugIt.session { DEBUGGER__::SESSION.enter_postmortem_session(copy) }
140
+ end
141
+ raise error
142
+ end
52
143
  end
53
144
  end
54
145
  end
55
146
  end
56
147
 
57
148
  RSpec::Core::Example.prepend RSpec::Debugging::DebugIt
58
- RSpec::Core::ExampleGroup.define_example_method(:dit, debug: true, focus: true)
149
+ # a gem install is already hidden by RSpec's /gems/ pattern; this covers a path or git checkout
150
+ RSpec.configuration.backtrace_exclusion_patterns << RSpec::Debugging::DebugIt::GEM_LIB
151
+ # :dit marks the example itself; :debug is also inherited from ddescribe/dcontext
152
+ RSpec::Core::ExampleGroup.define_example_method(:dit, debug: true, focus: true, dit: true)
59
153
  RSpec::Core::ExampleGroup.define_example_group_method(:ddescribe, debug: true, focus: true)
60
154
  RSpec::Core::ExampleGroup.define_example_group_method(:dcontext, debug: true, focus: true)
@@ -2,62 +2,84 @@ module RSpec
2
2
  module Debugging
3
3
  module LetVariables
4
4
  def let_variables
5
- let_variable_methods(self).keys
5
+ Internal.definitions(self).keys
6
6
  end
7
7
 
8
8
  def let_variable_initialized?(name)
9
- __memoized.instance_variable_get("@memoized").key?(name)
9
+ Internal.memoized(self).key?(Internal.resolve(self, name))
10
10
  end
11
11
 
12
12
  def let_variable_get(name)
13
- __memoized.instance_variable_get("@memoized")[name]
13
+ Internal.memoized(self)[Internal.resolve(self, name)]
14
14
  end
15
15
 
16
16
  def let_variable_reset(name)
17
- __memoized.instance_variable_get("@memoized").delete(name)
17
+ Internal.memoized(self).delete(Internal.resolve(self, name))
18
18
  end
19
19
 
20
20
  def let_variable_locations(name)
21
- let_variable_methods(self)[name].map do |fn|
22
- normalize_path(fn.source_location.join(":"))
21
+ Internal.definitions(self)[Internal.resolve(self, name)].map do |fn|
22
+ Internal.location(fn)
23
23
  end
24
24
  end
25
25
 
26
26
  def let_variable_values(name)
27
- let_variable_methods(self)[name].map do |fn|
28
- {
29
- normalize_path(fn.source_location.join(":")) => fn.bind(self).call
30
- }
27
+ memo = Internal.memoized(self)
28
+
29
+ name = Internal.resolve(self, name)
30
+
31
+ Internal.definitions(self)[name].map do |fn|
32
+ snapshot = memo.dup
33
+ memo.delete(name) # else super() reads the memoized value via the outer wrapper
34
+ value = begin
35
+ # mirrors rspec-core's `let`, which passes the example to a 1-arity block
36
+ fn.arity == 1 ? fn.bind_call(self, RSpec.current_example) : fn.bind_call(self)
37
+ rescue => e
38
+ e
39
+ ensure
40
+ memo.replace(snapshot) # forget lets the definition pulled in
41
+ end
42
+
43
+ { Internal.location(fn) => value }
31
44
  end
32
45
  end
33
46
 
34
- private
47
+ # Helpers live here rather than in the mixin, which is included into
48
+ # every example group and would collide with users' own methods.
49
+ module Internal
50
+ module_function
35
51
 
36
- extend self
52
+ # let name => definitions, innermost scope first
53
+ def definitions(example)
54
+ mods = example.class.ancestors.select { |x| x.name&.end_with?("::LetDefinitions") }
37
55
 
38
- def let_variable_methods(example)
39
- # find RSpec modules containing `let` blocks
40
- mods = self.class.ancestors.select {|x| x.to_s.include?('::LetDefinitions') }
56
+ mods.each_with_object({}) do |mod, defs|
57
+ mod.instance_methods.each do |name|
58
+ (defs[name] ||= []) << mod.instance_method(name)
59
+ end
60
+ end
61
+ end
41
62
 
42
- locations = {}
63
+ def resolve(example, name)
64
+ name = name.to_sym
65
+ # a named subject is an alias of its let
66
+ name = example.class.instance_method(:subject).original_name if name == :subject
67
+ return name if definitions(example).key?(name)
43
68
 
44
- mods.each do |mod|
45
- mod.instance_methods.each do |method_name|
46
- locations[method_name] ||= []
47
- locations[method_name] << mod.instance_method(method_name)
48
- end
69
+ raise ArgumentError, "no let variable named #{name.inspect}"
49
70
  end
50
71
 
51
- locations
52
- end
72
+ def memoized(example)
73
+ # before(:context) swaps in a memo object with no hash
74
+ example.instance_variable_get(:@__memoized).instance_variable_get(:@memoized) || {}
75
+ end
53
76
 
54
- def normalize_path(path)
55
- path.delete_prefix(
56
- Dir.pwd + "/"
57
- ).gsub(
58
- %r{.*/gems/},
59
- ".../gems/"
60
- ).sub(Dir.home, "~")
77
+ def location(fn)
78
+ fn.source_location.join(":")
79
+ .delete_prefix(Dir.pwd + "/")
80
+ .gsub(%r{.*/gems/}, ".../gems/")
81
+ .sub(Dir.home, "~")
82
+ end
61
83
  end
62
84
  end
63
85
  end
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module Debugging
3
- VERSION = "0.0.4"
3
+ VERSION = "0.0.6"
4
4
  end
5
5
  end
@@ -1,4 +1,4 @@
1
- require "rspec/expectations"
1
+ require "rspec/core"
2
2
 
3
3
  require "rspec/debugging/debug_it"
4
4
  require "rspec/debugging/let_variables"
@@ -1,3 +1,5 @@
1
+ require_relative "lib/rspec/debugging/version"
2
+
1
3
  Gem::Specification.new do |s|
2
4
  s.authors = ["Daniel Pepper"]
3
5
  s.description = "Tools to improve debugging in RSpec"
@@ -6,13 +8,19 @@ Gem::Specification.new do |s|
6
8
  s.license = "MIT"
7
9
  s.name = File.basename(__FILE__, ".gemspec")
8
10
  s.summary = s.description
9
- s.version = "0.0.4"
11
+ s.version = RSpec::Debugging::VERSION
12
+
13
+ s.metadata = {
14
+ "changelog_uri" => "#{s.homepage}/blob/main/CHANGELOG.md",
15
+ "rubygems_mfa_required" => "true",
16
+ "source_code_uri" => s.homepage,
17
+ }
10
18
 
11
- s.required_ruby_version = ">= 3.1"
19
+ s.required_ruby_version = ">= 3.2"
12
20
 
13
- s.add_dependency "rspec-expectations", ">= 3"
21
+ s.add_dependency "debug", ">= 1.10"
22
+ s.add_dependency "rspec-core", ">= 3.10"
14
23
 
15
- s.add_development_dependency "debug"
16
24
  s.add_development_dependency "rspec"
17
25
  s.add_development_dependency "simplecov"
18
26
  end
metadata CHANGED
@@ -1,42 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-debugging
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rspec-expectations
13
+ name: debug
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '3'
18
+ version: '1.10'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '3'
25
+ version: '1.10'
26
26
  - !ruby/object:Gem::Dependency
27
- name: debug
27
+ name: rspec-core
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '0'
33
- type: :development
32
+ version: '3.10'
33
+ type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '3.10'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rspec
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -84,7 +84,10 @@ files:
84
84
  homepage: https://github.com/dpep/rspec-debugging
85
85
  licenses:
86
86
  - MIT
87
- metadata: {}
87
+ metadata:
88
+ changelog_uri: https://github.com/dpep/rspec-debugging/blob/main/CHANGELOG.md
89
+ rubygems_mfa_required: 'true'
90
+ source_code_uri: https://github.com/dpep/rspec-debugging
88
91
  rdoc_options: []
89
92
  require_paths:
90
93
  - lib
@@ -92,14 +95,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
95
  requirements:
93
96
  - - ">="
94
97
  - !ruby/object:Gem::Version
95
- version: '3.1'
98
+ version: '3.2'
96
99
  required_rubygems_version: !ruby/object:Gem::Requirement
97
100
  requirements:
98
101
  - - ">="
99
102
  - !ruby/object:Gem::Version
100
103
  version: '0'
101
104
  requirements: []
102
- rubygems_version: 3.6.3
105
+ rubygems_version: 4.0.16
103
106
  specification_version: 4
104
107
  summary: Tools to improve debugging in RSpec
105
108
  test_files: []