spec_forge 0.4.0 → 0.6.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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -0
  3. data/CHANGELOG.md +145 -1
  4. data/README.md +49 -638
  5. data/flake.lock +3 -3
  6. data/flake.nix +8 -2
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +141 -12
  9. data/lib/spec_forge/attribute/faker.rb +64 -15
  10. data/lib/spec_forge/attribute/global.rb +96 -0
  11. data/lib/spec_forge/attribute/literal.rb +15 -2
  12. data/lib/spec_forge/attribute/matcher.rb +188 -13
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -20
  14. data/lib/spec_forge/attribute/regex.rb +55 -5
  15. data/lib/spec_forge/attribute/resolvable.rb +48 -5
  16. data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
  17. data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
  18. data/lib/spec_forge/attribute/store.rb +65 -0
  19. data/lib/spec_forge/attribute/transform.rb +33 -5
  20. data/lib/spec_forge/attribute/variable.rb +37 -6
  21. data/lib/spec_forge/attribute.rb +168 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +79 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/init.rb +11 -1
  27. data/lib/spec_forge/cli/new.rb +54 -3
  28. data/lib/spec_forge/cli/run.rb +20 -0
  29. data/lib/spec_forge/cli.rb +16 -5
  30. data/lib/spec_forge/configuration.rb +94 -25
  31. data/lib/spec_forge/context/callbacks.rb +91 -0
  32. data/lib/spec_forge/context/global.rb +72 -0
  33. data/lib/spec_forge/context/store.rb +148 -0
  34. data/lib/spec_forge/context/variables.rb +91 -0
  35. data/lib/spec_forge/context.rb +36 -0
  36. data/lib/spec_forge/core_ext/rspec.rb +24 -4
  37. data/lib/spec_forge/error.rb +267 -113
  38. data/lib/spec_forge/factory.rb +33 -14
  39. data/lib/spec_forge/filter.rb +87 -0
  40. data/lib/spec_forge/forge.rb +170 -0
  41. data/lib/spec_forge/http/backend.rb +99 -29
  42. data/lib/spec_forge/http/client.rb +23 -13
  43. data/lib/spec_forge/http/request.rb +74 -62
  44. data/lib/spec_forge/http/verb.rb +79 -0
  45. data/lib/spec_forge/http.rb +105 -0
  46. data/lib/spec_forge/loader.rb +254 -0
  47. data/lib/spec_forge/matchers.rb +130 -0
  48. data/lib/spec_forge/normalizer/configuration.rb +24 -11
  49. data/lib/spec_forge/normalizer/constraint.rb +22 -9
  50. data/lib/spec_forge/normalizer/expectation.rb +31 -12
  51. data/lib/spec_forge/normalizer/factory.rb +24 -11
  52. data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
  53. data/lib/spec_forge/normalizer/global_context.rb +88 -0
  54. data/lib/spec_forge/normalizer/spec.rb +39 -16
  55. data/lib/spec_forge/normalizer.rb +255 -41
  56. data/lib/spec_forge/runner/callbacks.rb +246 -0
  57. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  58. data/lib/spec_forge/runner/listener.rb +54 -0
  59. data/lib/spec_forge/runner/metadata.rb +58 -0
  60. data/lib/spec_forge/runner/state.rb +99 -0
  61. data/lib/spec_forge/runner.rb +133 -119
  62. data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
  63. data/lib/spec_forge/spec/expectation.rb +43 -51
  64. data/lib/spec_forge/spec.rb +83 -96
  65. data/lib/spec_forge/type.rb +36 -4
  66. data/lib/spec_forge/version.rb +4 -1
  67. data/lib/spec_forge.rb +161 -76
  68. metadata +20 -5
  69. data/spec_forge/factories/user.yml +0 -4
  70. data/spec_forge/forge_helper.rb +0 -37
  71. data/spec_forge/specs/users.yml +0 -65
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Creates a debugging environment during test execution.
7
+ # When a breakpoint is triggered, this provides an interface to inspect
8
+ # the current test state including the request, response, variables, and expectations.
9
+ #
10
+ # By default, this outputs a JSON representation of the current testing context,
11
+ # but it can be customized by configuring SpecForge.configuration.on_debug
12
+ # to use any Ruby debugger (like pry or debug).
13
+ #
14
+ # @example Basic usage in a spec with `debug: true`
15
+ # # In your YAML test:
16
+ # get_users:
17
+ # debug: true
18
+ # path: /users
19
+ # expectations:
20
+ # - expect:
21
+ # status: 200
22
+ #
23
+ # @example Custom debug handler in forge_helper.rb
24
+ # SpecForge.configure do |config|
25
+ # config.on_debug { binding.pry } # Requires 'pry' gem
26
+ # end
27
+ #
28
+ class DebugProxy
29
+ #
30
+ # @return [Proc] The default debugging handler that outputs JSON state information
31
+ #
32
+ def self.default
33
+ -> { puts inspect }
34
+ end
35
+
36
+ # @return [RSpec::Forge] The current Forge that is being tested
37
+ attr_reader :forge
38
+
39
+ # @return [SpecForge::Spec] The current Spec that is being tested
40
+ attr_reader :spec
41
+
42
+ # @return [SpecForge::Spec::Expectation] The current expectation that is being tested
43
+ attr_reader :expectation
44
+
45
+ # @return [RSpec::ExampleGroup] The current RSpec example group
46
+ attr_reader :example_group
47
+
48
+ # @return [RSpec::Example] The current RSpec example that is running
49
+ attr_reader :example
50
+
51
+ # @return [Integer] The expected HTTP status code
52
+ attr_reader :expected_status
53
+
54
+ # @return [Object] The expected response body structure
55
+ attr_reader :expected_json
56
+
57
+ delegate_missing_to :@example_group
58
+
59
+ #
60
+ # Creates a new DebugProxy instance
61
+ #
62
+ # @param forge [SpecForge::Forge] The forge being tested
63
+ # @param spec [SpecForge::Spec] The spec being tested
64
+ # @param expectation [SpecForge::Spec::Expectation] The expectation being tested
65
+ # @param example_group [RSpec::Core::ExampleGroup] The current example group
66
+ #
67
+ # @return [SpecForge::Runner::DebugProxy]
68
+ #
69
+ def initialize(forge, spec, expectation, example_group)
70
+ @callback = SpecForge.configuration.on_debug
71
+
72
+ @forge = forge
73
+ @spec = spec
74
+ @expectation = expectation
75
+ @example_group = example_group
76
+ @example = RSpec.current_example
77
+
78
+ constraints = expectation.constraints
79
+
80
+ @expected_status = constraints.status.resolved
81
+ @expected_json = constraints.json.resolved
82
+ end
83
+
84
+ #
85
+ # Triggers the debugging environment
86
+ #
87
+ # Displays available debugging contexts and executes the configured debug callback.
88
+ # The callback runs in the context of this proxy, giving it access to all helper methods.
89
+ #
90
+ # @return [void]
91
+ #
92
+ def call
93
+ puts <<~STRING
94
+
95
+ Debug triggered for:
96
+ > #{example.metadata[:rerun_file_path]} on line #{expectation.line_number}
97
+
98
+ Available debugging contexts:
99
+ - spec: Current spec details
100
+ - expectation: Current expectation being tested
101
+ - variables: Variables defined for this test
102
+ - global: Global context shared across tests
103
+ - store: Stored data from expectations
104
+
105
+ Request & Response:
106
+ - request: HTTP request details (method, url, headers, body)
107
+ - response: HTTP response with headers, status and body
108
+
109
+ Expectations:
110
+ - expected_status: Expected HTTP status code
111
+ - expected_json: Expected response body structure
112
+
113
+ Matchers:
114
+ - match_status: Matcher used to test status
115
+ - match_json: Matcher used to test response body
116
+
117
+ Helper objects:
118
+ - http_client: The HTTP client used for the request
119
+ - request_data: Raw request configuration data
120
+ - example_group: Current RSpec example group
121
+ - example: Current RSpec example
122
+ - forge: Current file being tested
123
+
124
+ 💡 Pro tips:
125
+ - Type 'self' or 'inspect' for a pretty-printed JSON overview
126
+ - Use 'to_h' for the hash representation
127
+ - Access the shared context with 'SpecForge.context'
128
+ STRING
129
+
130
+ instance_exec(&@callback)
131
+ end
132
+
133
+ ##########################################################################
134
+
135
+ #
136
+ # Returns a hash representation of the global context
137
+ #
138
+ # @return [Hash] The global context with resolved variables
139
+ #
140
+ def global
141
+ @global ||= SpecForge.context.global.to_h
142
+ end
143
+
144
+ #
145
+ # Returns a hash representation of the variables in the current context
146
+ #
147
+ # Includes both spec-level and expectation-level variables combined
148
+ # with values fully resolved.
149
+ #
150
+ # @return [Hash]
151
+ #
152
+ def variables
153
+ @variables ||= SpecForge.context.variables
154
+ end
155
+
156
+ #
157
+ # Returns a hash representation of the store context
158
+ #
159
+ # @return [Hash] The store context
160
+ #
161
+ def store
162
+ @store ||= SpecForge.context.store.to_h
163
+ end
164
+
165
+ ##########################################################################
166
+
167
+ #
168
+ # Returns a hash representation of the test state
169
+ #
170
+ # Includes the spec, expectation, request, response, variables and global context.
171
+ # RSpec matchers are converted to human-readable descriptions.
172
+ #
173
+ # @return [Hash]
174
+ #
175
+ def to_h
176
+ spec_hash = spec.to_h.except(:expectations)
177
+
178
+ expectation_hash = expectation.to_h
179
+ expectation_hash[:expect][:json] = matchers_to_description(expectation_hash[:expect][:json])
180
+
181
+ {
182
+ response: {
183
+ status: response.status,
184
+ body: response.body,
185
+ headers: response.headers
186
+ },
187
+ global:,
188
+ variables:,
189
+ request: request.to_h,
190
+ expectation: expectation_hash,
191
+ spec: spec_hash
192
+ }
193
+ end
194
+
195
+ #
196
+ # Returns a formatted JSON representation of the test state
197
+ #
198
+ # @return [String] Pretty-printed JSON of the test state
199
+ #
200
+ def inspect
201
+ JSON.pretty_generate(to_h)
202
+ end
203
+
204
+ private
205
+
206
+ def matchers_to_description(value)
207
+ return value unless value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
208
+
209
+ value.description
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Listens for RSpec test result notifications and triggers the appropriate callbacks
7
+ #
8
+ # This singleton class receives notifications from RSpec when examples pass or fail,
9
+ # retrieves the current test context, and triggers the appropriate SpecForge callbacks.
10
+ # It acts as a bridge between RSpec's notification system and SpecForge's callback system.
11
+ #
12
+ class Listener
13
+ include Singleton
14
+
15
+ #
16
+ # Handles RSpec notifications for passing examples
17
+ #
18
+ # @param notification [RSpec::Core::Notifications::ExampleNotification]
19
+ # The notification object
20
+ #
21
+ def example_passed(notification)
22
+ trigger_callback
23
+ end
24
+
25
+ #
26
+ # Handles RSpec notifications for failing examples
27
+ #
28
+ # @param notification [RSpec::Core::Notifications::FailedExampleNotification]
29
+ # The notification object
30
+ #
31
+ def example_failed(notification)
32
+ trigger_callback
33
+ end
34
+
35
+ private
36
+
37
+ #
38
+ # Triggers the appropriate SpecForge callback with the complete context
39
+ #
40
+ # Retrieves the current example context stored during the RSpec execution, and passes
41
+ # everything to the appropriate callback.
42
+ #
43
+ # @private
44
+ #
45
+ def trigger_callback
46
+ context = Runner::State.current.to_h.slice(
47
+ :forge, :spec, :expectation, :example_group, :example
48
+ )
49
+
50
+ Runner::Callbacks.after_expectation(*context.values)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Manages metadata for RSpec example groups and examples
7
+ #
8
+ # This class provides methods for setting up correct metadata on RSpec groups
9
+ # and examples, which enables proper error reporting and command-line rerun
10
+ # instructions when tests fail.
11
+ #
12
+ # @example Setting metadata on an example group
13
+ # Metadata.set_for_group(spec, expectation, example_group)
14
+ #
15
+ class Metadata
16
+ class << self
17
+ #
18
+ # Updates the example group metadata for error reporting
19
+ #
20
+ # Sets the file path, line number, and location information in the
21
+ # example group's metadata. This ensures that RSpec can generate the
22
+ # proper command to rerun the failing tests.
23
+ #
24
+ # @param spec [SpecForge::Spec] The spec being tested
25
+ # @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
26
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to update
27
+ #
28
+ def set_for_group(spec, expectation, example_group)
29
+ metadata = {
30
+ file_path: spec.file_path,
31
+ absolute_file_path: spec.file_path,
32
+ line_number: expectation.line_number,
33
+ location: spec.file_path,
34
+ rerun_file_path: "#{spec.file_name}:#{spec.name}:\"#{expectation.name}\""
35
+ }
36
+
37
+ example_group.metadata.merge!(metadata)
38
+ end
39
+
40
+ #
41
+ # Updates the current example's metadata for error reporting
42
+ #
43
+ # Sets location information on the currently running example.
44
+ # This helps RSpec generate more accurate error messages when
45
+ # an exception occurs during test execution.
46
+ #
47
+ # @param spec [SpecForge::Spec] The spec being tested
48
+ # @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
49
+ #
50
+ def set_for_example(spec, expectation)
51
+ metadata = {location: "#{spec.file_path}:#{expectation.line_number}"}
52
+
53
+ RSpec.current_example.metadata.merge!(metadata)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Maintains test execution state to prevent duplicate HTTP requests
7
+ #
8
+ # This singleton class captures and preserves references to the current test context,
9
+ # including request and response objects that would otherwise be re-evaluated
10
+ # when accessed after RSpec clears its memoized variables. It solves a specific
11
+ # issue where accessing response data in after_expectation callbacks would
12
+ # trigger duplicate HTTP requests.
13
+ #
14
+ class State < Struct.new(
15
+ :forge, :spec, :expectation, :example_group, :example, :response, :request
16
+ )
17
+ include Singleton
18
+
19
+ #
20
+ # Returns the singleton instance representing the current test state
21
+ #
22
+ # @return [State] The current state instance
23
+ #
24
+ def self.current
25
+ instance
26
+ end
27
+
28
+ #
29
+ # Updates multiple attributes of the state at once
30
+ #
31
+ # @param attributes [Hash] A hash mapping attribute names to values
32
+ #
33
+ def self.set(attributes)
34
+ attributes.each do |key, value|
35
+ instance[key] = value
36
+ end
37
+ end
38
+
39
+ #
40
+ # Persists the current state to the context store if needed
41
+ #
42
+ # Only runs if the current expectation has a store_as directive
43
+ #
44
+ def self.persist
45
+ return unless instance.expectation.store_as?
46
+
47
+ instance.persist_to_store
48
+ end
49
+
50
+ #
51
+ # Clears all state attributes
52
+ #
53
+ def self.clear
54
+ instance.clear
55
+ end
56
+
57
+ ##########################################################################
58
+
59
+ #
60
+ # Clears all attributes in the state
61
+ #
62
+ def clear
63
+ members.each { |key| self[key] = nil }
64
+ end
65
+
66
+ #
67
+ # Persists the current test execution data to the context store
68
+ #
69
+ # Handles scope determination and stores request/response data
70
+ # for later access via the store attribute
71
+ #
72
+ def persist_to_store
73
+ id = expectation.store_as
74
+ scope = :file
75
+
76
+ # Remove the file prefix if it was explicitly provided
77
+ id = id.delete_prefix("file.") if id.start_with?("file.")
78
+
79
+ # Change scope to spec if desired
80
+ if id.start_with?("spec.")
81
+ id = id.delete_prefix("spec.")
82
+ scope = :spec
83
+ end
84
+
85
+ SpecForge.context.store.set(
86
+ id,
87
+ scope:,
88
+ request: request&.to_h,
89
+ variables: SpecForge.context.variables.deep_dup,
90
+ response: {
91
+ headers: response&.headers,
92
+ status: response&.status,
93
+ body: response&.body
94
+ }
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,58 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecForge
4
+ #
5
+ # Handles the execution of specs through RSpec
6
+ # Converts SpecForge specs into RSpec examples and runs them
7
+ #
4
8
  class Runner
5
9
  class << self
6
10
  #
7
- # Runs any specs
11
+ # Defines RSpec examples for a collection of forges
12
+ # Creates the test structure that will be executed
8
13
  #
9
- def run
10
- RSpec::Core::Runner.disable_autorun!
11
- RSpec::Core::Runner.run([], $stderr, $stdout)
14
+ # @param forges [Array<Forge>] The forges to define as RSpec examples
15
+ #
16
+ def define(forges)
17
+ forges.each do |forge|
18
+ define_forge(forge)
19
+ end
12
20
  end
13
21
 
14
22
  #
15
- # Defines a spec with RSpec
23
+ # Runs the defined RSpec examples
24
+ # Executes the tests after they've been defined
16
25
  #
17
- # @param spec_forge [Spec] The spec to define
18
- #
19
- def define_spec(spec_forge)
20
- runner_forge = self
21
-
22
- RSpec.describe(spec_forge.name) do
23
- spec_forge.expectations.each do |expectation|
24
- # Define the example group
25
- describe(expectation.name) do
26
- # Set up the class metadata for error reporting
27
- runner_forge.set_group_metadata(self, spec_forge, expectation)
28
-
29
- constraints = expectation.constraints
30
-
31
- let!(:expected_status) { constraints.status.resolve }
32
- let!(:expected_json) { constraints.json.resolve.deep_stringify_keys }
33
-
34
- before do
35
- # Ensure all variables are called and resolved, in case they are not referenced
36
- expectation.variables.resolve
37
-
38
- # Set up the example metadata for error reporting
39
- runner_forge.set_example_metadata(spec_forge, expectation)
40
- end
41
-
42
- subject(:response) { expectation.http_client.call }
43
-
44
- it do
45
- if spec_forge.debug? || expectation.debug?
46
- runner_forge.handle_debug(expectation, self)
47
- end
26
+ def run
27
+ prepare_for_run
48
28
 
49
- # Status check
50
- expect(response.status).to eq(expected_status)
29
+ ARGV.clear
30
+ RSpec::Core::Runner.invoke
31
+ end
51
32
 
52
- # JSON check
53
- if constraints.json.size > 0
54
- expect(response.body).to be_kind_of(Hash)
55
- expect(response.body).to include(expected_json)
33
+ #
34
+ # Defines RSpec examples for a specific forge
35
+ # Creates the test structure for a single forge file
36
+ #
37
+ # @param forge [Forge] The forge to define
38
+ #
39
+ def define_forge(forge)
40
+ # This is just like writing a normal RSpec test, except with loops ;)
41
+ RSpec.describe(forge.name) do
42
+ # Callback for the file
43
+ before(:context) { Callbacks.before_file(forge) }
44
+ after(:context) { Callbacks.after_file(forge) }
45
+
46
+ # Specs
47
+ forge.specs.each do |spec|
48
+ # Describe the spec
49
+ describe(spec.name) do
50
+ # Request data is for the spec and contains the base and overlays
51
+ let!(:request_data) { forge.request[spec.id] }
52
+
53
+ # The HTTP client for the spec
54
+ let!(:http_client) { HTTP::Client.new(**request_data[:base]) }
55
+
56
+ # Callback for the spec
57
+ before(:context) { Callbacks.before_spec(forge, spec) }
58
+ after(:context) { Callbacks.after_spec(forge, spec) }
59
+
60
+ # Expectations
61
+ spec.expectations.each do |expectation|
62
+ # Onto the actual expectation itself
63
+ describe(expectation.name) do
64
+ # Set metadata for the example group for error reporting
65
+ Metadata.set_for_group(spec, expectation, self)
66
+
67
+ # Lazily load the constraints
68
+ let(:constraints) { expectation.constraints.as_matchers }
69
+
70
+ let(:match_status) { constraints[:status] }
71
+ let(:match_json) { constraints[:json] }
72
+ let(:match_json_class) { be_kind_of(match_json.class) }
73
+
74
+ # The request for the test itself. Overlays the expectation's data if it exists
75
+ let(:request) do
76
+ request = request_data[:base]
77
+
78
+ if (overlay = request_data[:overlay][expectation.id])
79
+ request = request.deep_merge(overlay)
80
+ end
81
+
82
+ HTTP::Request.new(**request)
83
+ end
84
+
85
+ # The Faraday response
86
+ subject(:response) { http_client.call(request) }
87
+
88
+ # Callbacks for the expectation
89
+ before :each do
90
+ Callbacks.before_expectation(
91
+ forge, spec, expectation, self, RSpec.current_example
92
+ )
93
+ end
94
+
95
+ # The 'after_expectation' callback is handled by Listener due to RSpec not
96
+ # reporting the example's status until after the describe block has finished.
97
+ after :each do
98
+ # However, the downside about having the callback triggered later is that RSpec
99
+ # will have reset the memoized let variables back to nil.
100
+ # This causes an issue when an expectation goes to store the state, it will end
101
+ # up re-calling the various variables and triggering another HTTP request.
102
+ # Since the variables are still memoized in this hook, it is the perfect
103
+ # time to store the referenced to them.
104
+ State.set(response:)
105
+ end
106
+
107
+ # The test itself
108
+ it(expectation.constraints.description) do
109
+ if spec.debug? || expectation.debug?
110
+ Callbacks.on_debug(forge, spec, expectation, self)
111
+ end
112
+
113
+ # Status check
114
+ expect(response.status).to match_status
115
+
116
+ # JSON check
117
+ if match_json.present?
118
+ expect(response.body).to match_json_class
119
+
120
+ case match_json
121
+ when Hash
122
+ # Check per key for easier debugging
123
+ match_json.each do |key, matcher|
124
+ expect(response.body).to have_key(key)
125
+ expect(response.body[key]).to matcher
126
+ end
127
+ else
128
+ expect(response.body).to match_json
129
+ end
130
+ end
131
+ end
56
132
  end
57
133
  end
58
134
  end
@@ -60,86 +136,24 @@ module SpecForge
60
136
  end
61
137
  end
62
138
 
63
- # @private
64
- def handle_debug(...)
65
- DebugProxy.new(...).call
66
- end
67
-
68
- # @private
69
- def set_group_metadata(context, spec, expectation)
70
- metadata = {
71
- file_path: spec.file_path,
72
- absolute_file_path: spec.file_path,
73
- line_number: spec.line_number,
74
- location: spec.file_path,
75
- rerun_file_path: "#{spec.file_name}:#{spec.name}:\"#{expectation.name}\""
76
- }
77
-
78
- context.metadata.merge!(metadata)
79
- end
80
-
81
- # @private
82
- def set_example_metadata(spec, expectation)
83
- # This is needed when an error raises in an example
84
- metadata = {location: "#{spec.file_path}:#{spec.line_number}"}
85
-
86
- RSpec.current_example.metadata.merge!(metadata)
87
- end
88
- end
139
+ private
89
140
 
90
- ################################################################################################
141
+ def prepare_for_run
142
+ # Allows modifying the error backtrace reporting within rspec
143
+ RSpec.configuration.instance_variable_set(:@backtrace_formatter, BacktraceFormatter)
91
144
 
92
- class DebugProxy
93
- def self.default
94
- -> { puts inspect }
95
- end
96
-
97
- attr_reader :expectation, :variables, :expected_status, :expected_json, :request, :response
98
-
99
- def initialize(expectation, spec_context)
100
- @callback = SpecForge.configuration.on_debug
101
-
102
- @expected_status = spec_context.expected_status
103
- @expected_json = spec_context.expected_json
104
-
105
- @request = expectation.http_client.request
106
- @response = spec_context.response
107
-
108
- @variables = expectation.variables
109
- @expectation = expectation
110
- end
111
-
112
- def call
113
- puts <<~STRING
114
-
115
- Debug triggered for: #{expectation.name}
116
-
117
- Available methods:
118
- - expectation: Full expectation context
119
- - variables: Current variable definitions
120
- - expected_status: Expected HTTP status code (#{expected_status})
121
- - expected_json: Expected response body
122
- - request: HTTP request details (method, url, headers, body)
123
- - response: HTTP response
124
-
125
- Tip: Type 'self' for a JSON overview of the current state
126
- Individual methods return full object details for advanced debugging
127
- STRING
128
-
129
- instance_exec(&@callback)
130
- end
131
-
132
- def inspect
133
- hash = expectation.to_h
134
-
135
- hash[:response] = {
136
- headers: response.headers,
137
- status: response.status,
138
- body: response.body
139
- }
140
-
141
- JSON.pretty_generate(hash)
145
+ # Listen for passed/failed events to trigger the "after_each" callback
146
+ RSpec.configuration.reporter.register_listener(
147
+ Listener.instance,
148
+ :example_passed, :example_failed
149
+ )
142
150
  end
143
151
  end
144
152
  end
145
153
  end
154
+
155
+ require_relative "runner/callbacks"
156
+ require_relative "runner/debug_proxy"
157
+ require_relative "runner/listener"
158
+ require_relative "runner/metadata"
159
+ require_relative "runner/state"