spec_forge 0.7.0 → 1.0.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/CHANGELOG.md +139 -9
- data/README.md +125 -203
- data/bin/spec_forge +1 -1
- data/flake.lock +76 -4
- data/flake.nix +5 -4
- data/lib/spec_forge/attribute/chainable.rb +6 -6
- data/lib/spec_forge/attribute/environment.rb +45 -0
- data/lib/spec_forge/attribute/factory.rb +26 -17
- data/lib/spec_forge/attribute/faker.rb +6 -1
- data/lib/spec_forge/attribute/generate.rb +114 -0
- data/lib/spec_forge/attribute/literal.rb +1 -14
- data/lib/spec_forge/attribute/matcher.rb +6 -2
- data/lib/spec_forge/attribute/parameterized.rb +20 -22
- data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
- data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
- data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
- data/lib/spec_forge/attribute/template.rb +118 -0
- data/lib/spec_forge/attribute/transform.rb +14 -19
- data/lib/spec_forge/attribute/variable.rb +31 -31
- data/lib/spec_forge/attribute.rb +54 -100
- data/lib/spec_forge/blueprint.rb +27 -0
- data/lib/spec_forge/cli/docs/generate.rb +28 -8
- data/lib/spec_forge/cli/docs.rb +5 -2
- data/lib/spec_forge/cli/init.rb +4 -4
- data/lib/spec_forge/cli/new.rb +78 -27
- data/lib/spec_forge/cli/run.rb +84 -52
- data/lib/spec_forge/cli/serve.rb +6 -0
- data/lib/spec_forge/cli.rb +6 -14
- data/lib/spec_forge/configuration.rb +212 -78
- data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
- data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
- data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
- data/lib/spec_forge/documentation/builder.rb +77 -329
- data/lib/spec_forge/documentation/document/operation.rb +4 -4
- data/lib/spec_forge/documentation/document.rb +0 -6
- data/lib/spec_forge/documentation/generator.rb +88 -0
- data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +22 -6
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +29 -7
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
- data/lib/spec_forge/documentation/openapi.rb +40 -12
- data/lib/spec_forge/documentation.rb +1 -7
- data/lib/spec_forge/error.rb +215 -41
- data/lib/spec_forge/factory.rb +38 -18
- data/lib/spec_forge/forge/action.rb +41 -0
- data/lib/spec_forge/forge/actions/call.rb +33 -0
- data/lib/spec_forge/forge/actions/debug.rb +47 -0
- data/lib/spec_forge/forge/actions/expect.rb +44 -0
- data/lib/spec_forge/forge/actions/request.rb +65 -0
- data/lib/spec_forge/forge/actions/store.rb +31 -0
- data/lib/spec_forge/forge/callbacks.rb +80 -0
- data/lib/spec_forge/forge/context.rb +41 -0
- data/lib/spec_forge/forge/display.rb +503 -0
- data/lib/spec_forge/forge/hooks.rb +131 -0
- data/lib/spec_forge/forge/runner/array_io.rb +81 -0
- data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
- data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
- data/lib/spec_forge/forge/runner/reporter.rb +56 -0
- data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
- data/lib/spec_forge/forge/runner.rb +118 -0
- data/lib/spec_forge/forge/timer.rb +94 -0
- data/lib/spec_forge/forge/variables.rb +38 -0
- data/lib/spec_forge/forge.rb +207 -133
- data/lib/spec_forge/http/backend.rb +49 -143
- data/lib/spec_forge/http/client.rb +14 -17
- data/lib/spec_forge/http/request.rb +37 -84
- data/lib/spec_forge/http/verb.rb +4 -0
- data/lib/spec_forge/http.rb +0 -5
- data/lib/spec_forge/loader/filter.rb +85 -0
- data/lib/spec_forge/loader/step_processor.rb +282 -0
- data/lib/spec_forge/loader.rb +105 -220
- data/lib/spec_forge/normalizer/default.rb +1 -1
- data/lib/spec_forge/normalizer/structure.rb +140 -0
- data/lib/spec_forge/normalizer/transformers.rb +168 -0
- data/lib/spec_forge/normalizer/validators.rb +50 -8
- data/lib/spec_forge/normalizer.rb +76 -119
- data/lib/spec_forge/normalizers/callback.yml +38 -0
- data/lib/spec_forge/normalizers/configuration.yml +59 -9
- data/lib/spec_forge/normalizers/factory.yml +53 -2
- data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
- data/lib/spec_forge/normalizers/json_schema.yml +79 -0
- data/lib/spec_forge/normalizers/step.yml +506 -0
- data/lib/spec_forge/step/call.rb +36 -0
- data/lib/spec_forge/step/expect.rb +110 -0
- data/lib/spec_forge/step/source.rb +22 -0
- data/lib/spec_forge/step.rb +129 -0
- data/lib/spec_forge/type.rb +115 -66
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +44 -106
- data/lib/templates/forge_helper.rb.tt +43 -22
- data/lib/templates/new_blueprint.yml.tt +54 -0
- metadata +75 -44
- data/lib/spec_forge/attribute/global.rb +0 -96
- data/lib/spec_forge/attribute/store.rb +0 -65
- data/lib/spec_forge/backtrace_formatter.rb +0 -50
- data/lib/spec_forge/callbacks.rb +0 -88
- data/lib/spec_forge/context/callbacks.rb +0 -91
- data/lib/spec_forge/context/global.rb +0 -72
- data/lib/spec_forge/context/store.rb +0 -131
- data/lib/spec_forge/context/variables.rb +0 -91
- data/lib/spec_forge/context.rb +0 -36
- data/lib/spec_forge/core_ext/rspec.rb +0 -55
- data/lib/spec_forge/core_ext.rb +0 -5
- data/lib/spec_forge/documentation/generators/base.rb +0 -81
- data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
- data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
- data/lib/spec_forge/documentation/generators.rb +0 -17
- data/lib/spec_forge/documentation/loader.rb +0 -159
- data/lib/spec_forge/documentation/openapi/base.rb +0 -33
- data/lib/spec_forge/filter.rb +0 -86
- data/lib/spec_forge/normalizer/definition.rb +0 -248
- data/lib/spec_forge/normalizers/_shared.yml +0 -74
- data/lib/spec_forge/normalizers/constraint.yml +0 -8
- data/lib/spec_forge/normalizers/expectation.yml +0 -47
- data/lib/spec_forge/normalizers/global_context.yml +0 -28
- data/lib/spec_forge/normalizers/spec.yml +0 -50
- data/lib/spec_forge/runner/adapter.rb +0 -183
- data/lib/spec_forge/runner/callbacks.rb +0 -246
- data/lib/spec_forge/runner/debug_proxy.rb +0 -213
- data/lib/spec_forge/runner/listener.rb +0 -54
- data/lib/spec_forge/runner/metadata.rb +0 -58
- data/lib/spec_forge/runner/state.rb +0 -98
- data/lib/spec_forge/runner.rb +0 -75
- data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
- data/lib/spec_forge/spec/expectation.rb +0 -68
- data/lib/spec_forge/spec.rb +0 -68
- data/lib/templates/new_spec.yml.tt +0 -43
|
@@ -1,213 +0,0 @@
|
|
|
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
|
-
global:,
|
|
183
|
-
variables:,
|
|
184
|
-
request: request.to_h,
|
|
185
|
-
response: {
|
|
186
|
-
status: response.status,
|
|
187
|
-
body: response.body,
|
|
188
|
-
headers: response.headers
|
|
189
|
-
},
|
|
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
|
|
@@ -1,54 +0,0 @@
|
|
|
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
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
data/lib/spec_forge/runner.rb
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
#
|
|
5
|
-
# Handles the execution of specs through RSpec
|
|
6
|
-
# Converts SpecForge specs into RSpec examples and runs them
|
|
7
|
-
#
|
|
8
|
-
class Runner
|
|
9
|
-
class << self
|
|
10
|
-
#
|
|
11
|
-
# Prepares forge objects for test execution
|
|
12
|
-
#
|
|
13
|
-
# Loads the forge helper, registers factories, loads specs from files,
|
|
14
|
-
# applies filtering, and returns ready-to-run forge objects.
|
|
15
|
-
#
|
|
16
|
-
# @param file_name [String, nil] Optional file name filter
|
|
17
|
-
# @param spec_name [String, nil] Optional spec name filter
|
|
18
|
-
# @param expectation_name [String, nil] Optional expectation name filter
|
|
19
|
-
#
|
|
20
|
-
# @return [Array<Forge>] Array of prepared forge objects
|
|
21
|
-
#
|
|
22
|
-
def prepare(file_name: nil, spec_name: nil, expectation_name: nil)
|
|
23
|
-
load_forge_helper
|
|
24
|
-
|
|
25
|
-
# Load factories
|
|
26
|
-
Factory.load_and_register
|
|
27
|
-
|
|
28
|
-
# Load the specs from their files and create forges from them
|
|
29
|
-
forges = Loader.load_from_files.map { |f| Forge.new(*f) }
|
|
30
|
-
|
|
31
|
-
# Filter out the specs and expectations
|
|
32
|
-
forges = Filter.apply(forges, file_name:, spec_name:, expectation_name:)
|
|
33
|
-
|
|
34
|
-
# Tell the user that we filtered if we did
|
|
35
|
-
Filter.announce(forges, file_name:, spec_name:, expectation_name:)
|
|
36
|
-
|
|
37
|
-
forges
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
# Runs the prepared forges through RSpec
|
|
42
|
-
#
|
|
43
|
-
# Sets up the RSpec adapter and executes all tests, with optional
|
|
44
|
-
# exit behavior for CLI usage.
|
|
45
|
-
#
|
|
46
|
-
# @param forges [Array<Forge>] The forge objects to run
|
|
47
|
-
# @param exit_on_finish [Boolean] Whether to exit the process when complete
|
|
48
|
-
# @param exit_on_failure [Boolean] Whether to exit the process if any test fails
|
|
49
|
-
#
|
|
50
|
-
# @return [Integer, nil] Exit status if exit_on_finish is false
|
|
51
|
-
#
|
|
52
|
-
def run(forges, **)
|
|
53
|
-
Adapter.setup(forges)
|
|
54
|
-
Adapter.run(**)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def load_forge_helper
|
|
60
|
-
forge_helper = SpecForge.forge_path.join("forge_helper.rb")
|
|
61
|
-
require_relative forge_helper if File.exist?(forge_helper)
|
|
62
|
-
|
|
63
|
-
# Validate in case anything was changed
|
|
64
|
-
SpecForge.configuration.validate
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
require_relative "runner/adapter"
|
|
71
|
-
require_relative "runner/callbacks"
|
|
72
|
-
require_relative "runner/debug_proxy"
|
|
73
|
-
require_relative "runner/listener"
|
|
74
|
-
require_relative "runner/metadata"
|
|
75
|
-
require_relative "runner/state"
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
class Spec
|
|
5
|
-
class Expectation
|
|
6
|
-
#
|
|
7
|
-
# Represents the expected response constraints for an expectation
|
|
8
|
-
#
|
|
9
|
-
# A Constraint defines what the API response should look like,
|
|
10
|
-
# including status code and body content with support for matchers.
|
|
11
|
-
#
|
|
12
|
-
# @example In code
|
|
13
|
-
# constraint = Constraint.new(
|
|
14
|
-
# status: 200,
|
|
15
|
-
# headers: {response_header: "kind_of.string"},
|
|
16
|
-
# json: {name: {"matcher.eq" => "John"}}
|
|
17
|
-
# )
|
|
18
|
-
#
|
|
19
|
-
class Constraint < Data.define(:status, :headers, :json) # :xml, :html
|
|
20
|
-
#
|
|
21
|
-
# Creates a new constraint
|
|
22
|
-
#
|
|
23
|
-
# @param status [Integer, String] The expected HTTP status code, or reference to one
|
|
24
|
-
# @param headers [Hash] The expected headers with matchers
|
|
25
|
-
# @param json [Hash, Array] The expected JSON with matchers
|
|
26
|
-
#
|
|
27
|
-
# @return [Constraint] A new constraint instance
|
|
28
|
-
#
|
|
29
|
-
def initialize(status:, headers: {}, json: {})
|
|
30
|
-
super(
|
|
31
|
-
status: Attribute.from(status),
|
|
32
|
-
headers: Attribute.from(headers),
|
|
33
|
-
json: Attribute.from(json)
|
|
34
|
-
)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
# Converts the constraint to a hash with resolved values
|
|
39
|
-
#
|
|
40
|
-
# @return [Hash] Hash representation with resolved values
|
|
41
|
-
#
|
|
42
|
-
def to_h
|
|
43
|
-
super.transform_values(&:resolve)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
# Converts constraints to RSpec matchers for validation
|
|
48
|
-
#
|
|
49
|
-
# Transforms the defined constraints (status and JSON expectations) into
|
|
50
|
-
# appropriate RSpec matchers that can be used in test expectations.
|
|
51
|
-
# This method resolves all values and applies the appropriate matcher
|
|
52
|
-
# conversions to create a complete expectation structure.
|
|
53
|
-
#
|
|
54
|
-
# @return [Hash] A hash containing resolved matchers
|
|
55
|
-
#
|
|
56
|
-
# @example
|
|
57
|
-
# constraint = Constraint.new(status: 200, json: {name: "John"})
|
|
58
|
-
# matchers = constraint.as_matchers
|
|
59
|
-
# # => {status: eq(200), json: include("name" => eq("John"))}
|
|
60
|
-
#
|
|
61
|
-
def as_matchers
|
|
62
|
-
{
|
|
63
|
-
status: status.resolve_as_matcher,
|
|
64
|
-
json: resolve_json_matcher,
|
|
65
|
-
headers: resolve_hash_matcher(headers)
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
# Generates a human-readable description of what this constraint expects in the response
|
|
71
|
-
#
|
|
72
|
-
# Creates a description string for RSpec examples that clearly explains the expected
|
|
73
|
-
# status code and JSON structure. This makes test output more informative and helps
|
|
74
|
-
# developers understand what's being tested at a glance.
|
|
75
|
-
#
|
|
76
|
-
# @return [String] A human-readable description of the constraint expectations
|
|
77
|
-
#
|
|
78
|
-
# @example Status code with JSON object
|
|
79
|
-
# constraint.description
|
|
80
|
-
# # => "is expected to respond with \"200 OK\" and a JSON object that contains keys: \"id\", \"name\""
|
|
81
|
-
#
|
|
82
|
-
# @example Status code with JSON array
|
|
83
|
-
# constraint.description
|
|
84
|
-
# # => "is expected to respond with \"201 Created\" and a JSON array that contains 3 items"
|
|
85
|
-
#
|
|
86
|
-
def description
|
|
87
|
-
description = "is expected to respond with"
|
|
88
|
-
|
|
89
|
-
description += if status.is_a?(Attribute::Literal)
|
|
90
|
-
" #{HTTP.status_code_to_description(status.input).in_quotes}"
|
|
91
|
-
else
|
|
92
|
-
" the expected status code"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
size = json.size
|
|
96
|
-
|
|
97
|
-
if Type.array?(json)
|
|
98
|
-
description +=
|
|
99
|
-
" and a JSON array that contains #{size} #{"item".pluralize(size)}"
|
|
100
|
-
elsif Type.hash?(json) && size > 0
|
|
101
|
-
keys = json.keys.join_map(", ", &:in_quotes)
|
|
102
|
-
|
|
103
|
-
description +=
|
|
104
|
-
" and a JSON object that contains #{"key".pluralize(size)}: #{keys}"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
description
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
def resolve_json_matcher
|
|
113
|
-
case json
|
|
114
|
-
when HashLike
|
|
115
|
-
resolve_hash_matcher(json)
|
|
116
|
-
else
|
|
117
|
-
json.resolve_as_matcher
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def resolve_hash_matcher(hash)
|
|
122
|
-
hash.transform_values(&:resolve_as_matcher).stringify_keys
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
class Spec
|
|
5
|
-
#
|
|
6
|
-
# Represents a single test expectation within a spec
|
|
7
|
-
#
|
|
8
|
-
# An Expectation defines what should be tested for a specific API request,
|
|
9
|
-
# including the expected status code and response structure.
|
|
10
|
-
#
|
|
11
|
-
# @example YAML representation
|
|
12
|
-
# - name: "Get user successfully"
|
|
13
|
-
# expect:
|
|
14
|
-
# status: 200
|
|
15
|
-
# json:
|
|
16
|
-
# name: kind_of.string
|
|
17
|
-
#
|
|
18
|
-
class Expectation < Data.define(
|
|
19
|
-
:id, :name, :line_number,
|
|
20
|
-
:debug, :store_as, :documentation, :constraints
|
|
21
|
-
)
|
|
22
|
-
#
|
|
23
|
-
# @return [Boolean] True if debugging is enabled
|
|
24
|
-
#
|
|
25
|
-
attr_predicate :debug
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
# @return [Boolean] True if store_as is set
|
|
29
|
-
#
|
|
30
|
-
attr_predicate :store_as
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
# Creates a new expectation with constraints
|
|
34
|
-
#
|
|
35
|
-
# @param id [String] Unique identifier
|
|
36
|
-
# @param name [String] Human-readable name
|
|
37
|
-
# @param line_number [Integer] Line number in source
|
|
38
|
-
# @param debug [Boolean] Whether to enable debugging
|
|
39
|
-
# @param store_as [String] Unique Context::Store identifier
|
|
40
|
-
# @param documentation [Boolean] Whether to include in documentation generation
|
|
41
|
-
# @param expect [Hash] Expected constraints
|
|
42
|
-
#
|
|
43
|
-
# @return [Expectation] A new expectation instance
|
|
44
|
-
#
|
|
45
|
-
def initialize(id:, name:, line_number:, debug:, store_as:, expect:, documentation:)
|
|
46
|
-
constraints = Constraint.new(**expect)
|
|
47
|
-
|
|
48
|
-
super(id:, name:, line_number:, debug:, store_as:, documentation:, constraints:)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
# Converts the expectation to a hash representation
|
|
53
|
-
#
|
|
54
|
-
# @return [Hash] Hash representation
|
|
55
|
-
#
|
|
56
|
-
def to_h
|
|
57
|
-
{
|
|
58
|
-
name:,
|
|
59
|
-
line_number:,
|
|
60
|
-
debug:,
|
|
61
|
-
expect: constraints.to_h
|
|
62
|
-
}
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
require_relative "expectation/constraint"
|