spec_forge 0.5.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.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +106 -1
- data/README.md +34 -22
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +91 -14
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -22
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +21 -8
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +27 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +132 -123
- data/lib/spec_forge/spec/expectation/constraint.rb +91 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- 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
|