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 +4 -4
- data/CHANGELOG.md +31 -0
- data/Gemfile.lock +28 -16
- data/README.md +103 -28
- data/lib/rspec/debugging/debug_it.rb +126 -32
- data/lib/rspec/debugging/let_variables.rb +52 -30
- data/lib/rspec/debugging/version.rb +1 -1
- data/lib/rspec/debugging.rb +1 -1
- data/rspec-debugging.gemspec +12 -4
- metadata +15 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cef0952745e075d2ddb60bf6827985ae51dcc935b14624f97bebb48bda5b29f1
|
|
4
|
+
data.tar.gz: 104a0eae0841e4c02dc15004254f020c87f7f79aaa4058183c88a1d3bbe2ef54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
5
|
-
|
|
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
|
-
|
|
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.
|
|
16
|
+
diff-lcs (1.6.2)
|
|
14
17
|
docile (1.4.1)
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
25
|
+
pp (0.6.3)
|
|
26
|
+
prettyprint
|
|
27
|
+
prettyprint (0.2.0)
|
|
28
|
+
psych (5.3.1)
|
|
29
|
+
date
|
|
20
30
|
stringio
|
|
21
|
-
rdoc (
|
|
31
|
+
rdoc (7.0.1)
|
|
32
|
+
erb
|
|
22
33
|
psych (>= 4.0.0)
|
|
23
|
-
|
|
34
|
+
tsort
|
|
35
|
+
reline (0.6.3)
|
|
24
36
|
io-console (~> 0.5)
|
|
25
|
-
rspec (3.13.
|
|
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.
|
|
41
|
+
rspec-core (3.13.6)
|
|
30
42
|
rspec-support (~> 3.13.0)
|
|
31
|
-
rspec-expectations (3.13.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
+
`let` variables are great, but complexity can obscure their values.
|
|
45
101
|
|
|
46
102
|
```ruby
|
|
47
|
-
describe
|
|
48
|
-
|
|
49
|
-
subject { a }
|
|
103
|
+
describe "a" do
|
|
104
|
+
subject { a }
|
|
50
105
|
|
|
51
|
-
|
|
52
|
-
|
|
106
|
+
let(:a) { "a" }
|
|
107
|
+
let!(:b) { "b" }
|
|
53
108
|
|
|
54
|
-
|
|
109
|
+
it { is_expected.to eq ??? }
|
|
55
110
|
|
|
56
|
-
|
|
57
|
-
|
|
111
|
+
context "when we nest" do
|
|
112
|
+
let(:a) { "A" + b }
|
|
58
113
|
|
|
59
|
-
|
|
114
|
+
it { is_expected.to eq ??? }
|
|
60
115
|
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
context "...and nest" do
|
|
117
|
+
let(:a) { super() * 2 }
|
|
63
118
|
|
|
64
|
-
|
|
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`.
|
|
125
|
+
...add a hundred lines of legacy code and then solve for `a`. Or use tooling :)
|
|
70
126
|
|
|
71
|
-
|
|
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
|
-
*
|
|
77
|
-
|
|
144
|
+
* names of all `let` variables in scope (including `subject`)
|
|
78
145
|
|
|
79
146
|
### let_variable_initialized?(name)
|
|
80
|
-
* whether
|
|
81
|
-
|
|
147
|
+
* whether the variable has been evaluated yet in this example
|
|
82
148
|
|
|
83
149
|
### let_variable_get(name)
|
|
84
|
-
* the value
|
|
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
|
-
*
|
|
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
|
-
|
|
10
|
-
return super unless user_metadata[:debug] || ENABLED
|
|
9
|
+
GEM_LIB = %r{\A#{Regexp.escape(File.expand_path("../..", __dir__))}/}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
debugging: #{location}
|
|
75
|
+
super
|
|
76
|
+
end
|
|
41
77
|
|
|
42
|
-
|
|
78
|
+
private
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5
|
+
Internal.definitions(self).keys
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def let_variable_initialized?(name)
|
|
9
|
-
|
|
9
|
+
Internal.memoized(self).key?(Internal.resolve(self, name))
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def let_variable_get(name)
|
|
13
|
-
|
|
13
|
+
Internal.memoized(self)[Internal.resolve(self, name)]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def let_variable_reset(name)
|
|
17
|
-
|
|
17
|
+
Internal.memoized(self).delete(Internal.resolve(self, name))
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def let_variable_locations(name)
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
data/lib/rspec/debugging.rb
CHANGED
data/rspec-debugging.gemspec
CHANGED
|
@@ -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 =
|
|
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.
|
|
19
|
+
s.required_ruby_version = ">= 3.2"
|
|
12
20
|
|
|
13
|
-
s.add_dependency "
|
|
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
|
+
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:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: debug
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
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: '
|
|
25
|
+
version: '1.10'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: rspec-core
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
33
|
-
type: :
|
|
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: '
|
|
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.
|
|
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:
|
|
105
|
+
rubygems_version: 4.0.16
|
|
103
106
|
specification_version: 4
|
|
104
107
|
summary: Tools to improve debugging in RSpec
|
|
105
108
|
test_files: []
|