spec_forge 0.5.0 → 0.7.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 +217 -2
- data/README.md +162 -25
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +92 -15
- 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 +88 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/docs/generate.rb +72 -0
- data/lib/spec_forge/cli/docs.rb +92 -0
- data/lib/spec_forge/cli/init.rb +51 -9
- data/lib/spec_forge/cli/new.rb +67 -6
- data/lib/spec_forge/cli/run.rb +32 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +26 -7
- data/lib/spec_forge/configuration.rb +96 -24
- 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 +131 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/array.rb +27 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/documentation/builder.rb +383 -0
- data/lib/spec_forge/documentation/document/operation.rb +47 -0
- data/lib/spec_forge/documentation/document/parameter.rb +22 -0
- data/lib/spec_forge/documentation/document/request_body.rb +24 -0
- data/lib/spec_forge/documentation/document/response.rb +39 -0
- data/lib/spec_forge/documentation/document/response_body.rb +27 -0
- data/lib/spec_forge/documentation/document.rb +48 -0
- data/lib/spec_forge/documentation/generators/base.rb +81 -0
- data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
- data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
- data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
- data/lib/spec_forge/documentation/generators.rb +17 -0
- data/lib/spec_forge/documentation/loader/cache.rb +138 -0
- data/lib/spec_forge/documentation/loader.rb +159 -0
- data/lib/spec_forge/documentation/openapi/base.rb +33 -0
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
- data/lib/spec_forge/documentation/openapi.rb +23 -0
- data/lib/spec_forge/documentation.rb +27 -0
- data/lib/spec_forge/error.rb +284 -113
- data/lib/spec_forge/factory.rb +35 -16
- data/lib/spec_forge/filter.rb +86 -0
- data/lib/spec_forge/forge.rb +171 -0
- data/lib/spec_forge/http/backend.rb +101 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +85 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +244 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/default.rb +51 -0
- data/lib/spec_forge/normalizer/definition.rb +248 -0
- data/lib/spec_forge/normalizer/validators.rb +99 -0
- data/lib/spec_forge/normalizer.rb +486 -115
- data/lib/spec_forge/normalizers/_shared.yml +74 -0
- data/lib/spec_forge/normalizers/configuration.yml +23 -0
- data/lib/spec_forge/normalizers/constraint.yml +8 -0
- data/lib/spec_forge/normalizers/expectation.yml +47 -0
- data/lib/spec_forge/normalizers/factory.yml +12 -0
- data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
- data/lib/spec_forge/normalizers/global_context.yml +28 -0
- data/lib/spec_forge/normalizers/spec.yml +50 -0
- data/lib/spec_forge/runner/adapter.rb +183 -0
- 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 +98 -0
- data/lib/spec_forge/runner.rb +50 -125
- data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
- data/lib/spec_forge/spec/expectation.rb +47 -51
- data/lib/spec_forge/spec.rb +50 -108
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +168 -76
- data/lib/templates/openapi.yml.tt +22 -0
- data/lib/templates/redoc.html.tt +28 -0
- data/lib/templates/swagger.html.tt +59 -0
- metadata +109 -16
- data/lib/spec_forge/normalizer/configuration.rb +0 -77
- data/lib/spec_forge/normalizer/constraint.rb +0 -47
- data/lib/spec_forge/normalizer/expectation.rb +0 -86
- data/lib/spec_forge/normalizer/factory.rb +0 -65
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
- data/lib/spec_forge/normalizer/spec.rb +0 -74
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
- /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
- /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
- /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
id: string
|
2
|
+
|
3
|
+
name: string
|
4
|
+
|
5
|
+
line_number: integer
|
6
|
+
|
7
|
+
base_url:
|
8
|
+
type: string
|
9
|
+
default: null
|
10
|
+
required: false
|
11
|
+
|
12
|
+
url:
|
13
|
+
type: string
|
14
|
+
default: null
|
15
|
+
required: false
|
16
|
+
aliases:
|
17
|
+
- path
|
18
|
+
|
19
|
+
http_verb:
|
20
|
+
type: string
|
21
|
+
default: null # Do not default this to "GET". Leave it null. Seriously.
|
22
|
+
required: false
|
23
|
+
aliases:
|
24
|
+
- method
|
25
|
+
- http_method
|
26
|
+
validator: http_verb
|
27
|
+
|
28
|
+
headers:
|
29
|
+
type: hash
|
30
|
+
default: {}
|
31
|
+
required: false
|
32
|
+
|
33
|
+
query:
|
34
|
+
type:
|
35
|
+
- hash
|
36
|
+
- string
|
37
|
+
aliases:
|
38
|
+
- params
|
39
|
+
default: {}
|
40
|
+
required: false
|
41
|
+
|
42
|
+
body:
|
43
|
+
type:
|
44
|
+
- hash
|
45
|
+
- string
|
46
|
+
aliases:
|
47
|
+
- data
|
48
|
+
default: {}
|
49
|
+
required: false
|
50
|
+
|
51
|
+
variables:
|
52
|
+
type:
|
53
|
+
- hash
|
54
|
+
- string
|
55
|
+
default: {}
|
56
|
+
required: false
|
57
|
+
|
58
|
+
debug:
|
59
|
+
type: boolean
|
60
|
+
aliases:
|
61
|
+
- pry
|
62
|
+
- breakpoint
|
63
|
+
default: false
|
64
|
+
required: false
|
65
|
+
|
66
|
+
callback:
|
67
|
+
type: string
|
68
|
+
required: false
|
69
|
+
validator: callback
|
70
|
+
|
71
|
+
documentation:
|
72
|
+
type: boolean
|
73
|
+
required: false
|
74
|
+
default: true
|
@@ -0,0 +1,23 @@
|
|
1
|
+
base_url: string
|
2
|
+
|
3
|
+
headers:
|
4
|
+
reference: headers
|
5
|
+
|
6
|
+
query:
|
7
|
+
reference: query
|
8
|
+
|
9
|
+
factories:
|
10
|
+
type: hash
|
11
|
+
default: {}
|
12
|
+
structure:
|
13
|
+
###########################################
|
14
|
+
auto_discover:
|
15
|
+
type: boolean
|
16
|
+
default: true
|
17
|
+
|
18
|
+
paths:
|
19
|
+
type: array
|
20
|
+
default: []
|
21
|
+
|
22
|
+
on_debug:
|
23
|
+
type: proc
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Internal
|
2
|
+
id:
|
3
|
+
reference: id
|
4
|
+
|
5
|
+
line_number:
|
6
|
+
reference: line_number
|
7
|
+
|
8
|
+
# User defined
|
9
|
+
name:
|
10
|
+
reference: name
|
11
|
+
|
12
|
+
base_url:
|
13
|
+
reference: base_url
|
14
|
+
|
15
|
+
url:
|
16
|
+
reference: url
|
17
|
+
|
18
|
+
http_verb:
|
19
|
+
reference: http_verb
|
20
|
+
|
21
|
+
headers:
|
22
|
+
reference: headers
|
23
|
+
|
24
|
+
query:
|
25
|
+
reference: query
|
26
|
+
|
27
|
+
body:
|
28
|
+
reference: body
|
29
|
+
|
30
|
+
variables:
|
31
|
+
reference: variables
|
32
|
+
|
33
|
+
debug:
|
34
|
+
reference: debug
|
35
|
+
|
36
|
+
store_as:
|
37
|
+
type: string
|
38
|
+
default: ""
|
39
|
+
|
40
|
+
documentation:
|
41
|
+
reference: documentation
|
42
|
+
|
43
|
+
expect:
|
44
|
+
type: hash
|
45
|
+
structure:
|
46
|
+
###########################################
|
47
|
+
reference: constraint
|
@@ -0,0 +1,28 @@
|
|
1
|
+
variables:
|
2
|
+
reference: variables
|
3
|
+
|
4
|
+
callbacks:
|
5
|
+
type: array
|
6
|
+
default: []
|
7
|
+
structure:
|
8
|
+
###########################################
|
9
|
+
type: hash
|
10
|
+
default: {}
|
11
|
+
structure:
|
12
|
+
###########################################
|
13
|
+
before_file:
|
14
|
+
reference: callback
|
15
|
+
before_spec:
|
16
|
+
reference: callback
|
17
|
+
before_each:
|
18
|
+
reference: callback
|
19
|
+
aliases:
|
20
|
+
- before
|
21
|
+
after_each:
|
22
|
+
reference: callback
|
23
|
+
aliases:
|
24
|
+
- after
|
25
|
+
after_spec:
|
26
|
+
reference: callback
|
27
|
+
after_file:
|
28
|
+
reference: callback
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Internal
|
2
|
+
id:
|
3
|
+
reference: id
|
4
|
+
|
5
|
+
name:
|
6
|
+
reference: name
|
7
|
+
|
8
|
+
file_name: string
|
9
|
+
|
10
|
+
file_path: string
|
11
|
+
|
12
|
+
line_number:
|
13
|
+
reference: line_number
|
14
|
+
|
15
|
+
# User defined
|
16
|
+
base_url:
|
17
|
+
reference: base_url
|
18
|
+
|
19
|
+
url:
|
20
|
+
reference: url
|
21
|
+
|
22
|
+
http_verb:
|
23
|
+
reference: http_verb
|
24
|
+
|
25
|
+
headers:
|
26
|
+
reference: headers
|
27
|
+
|
28
|
+
query:
|
29
|
+
reference: query
|
30
|
+
|
31
|
+
body:
|
32
|
+
reference: body
|
33
|
+
|
34
|
+
variables:
|
35
|
+
reference: variables
|
36
|
+
|
37
|
+
debug:
|
38
|
+
reference: debug
|
39
|
+
|
40
|
+
documentation:
|
41
|
+
reference: documentation
|
42
|
+
|
43
|
+
expectations:
|
44
|
+
type: array
|
45
|
+
structure:
|
46
|
+
###########################################
|
47
|
+
type: hash
|
48
|
+
structure:
|
49
|
+
###########################################
|
50
|
+
reference: expectation
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Runner
|
5
|
+
#
|
6
|
+
# Bridges SpecForge specs with RSpec execution
|
7
|
+
#
|
8
|
+
# Converts SpecForge forge objects into RSpec test structures
|
9
|
+
# and manages the test execution lifecycle.
|
10
|
+
#
|
11
|
+
class Adapter
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
#
|
15
|
+
# Configures RSpec with forge definitions
|
16
|
+
#
|
17
|
+
# Sets up RSpec and prepares everything for running tests
|
18
|
+
#
|
19
|
+
# @param forges [Array<Forge>] The forges to set up for testing
|
20
|
+
#
|
21
|
+
def self.setup(forges)
|
22
|
+
# Defines the forges with RSpec
|
23
|
+
forges.each { |forge| instance.describe(forge) }
|
24
|
+
|
25
|
+
# Disable autorun because RSpec does it
|
26
|
+
RSpec::Core::Runner.disable_autorun!
|
27
|
+
|
28
|
+
# Allows modifying the error backtrace reporting within rspec
|
29
|
+
RSpec.configuration.instance_variable_set(:@backtrace_formatter, BacktraceFormatter)
|
30
|
+
|
31
|
+
# Listen for passed/failed events to trigger the "after_each" callback
|
32
|
+
RSpec.configuration.reporter.register_listener(
|
33
|
+
Listener.instance,
|
34
|
+
:example_passed, :example_failed
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Executes the configured RSpec tests
|
40
|
+
#
|
41
|
+
# Runs all configured tests through RSpec with optional exit behavior.
|
42
|
+
#
|
43
|
+
# @param exit_on_finish [Boolean] Whether to exit the process when done
|
44
|
+
# @param exit_on_failure [Boolean] Whether to exit the process if any test fails
|
45
|
+
#
|
46
|
+
# @return [Integer, nil] Exit status if exit_on_finish is false
|
47
|
+
#
|
48
|
+
def self.run(exit_on_finish: false, exit_on_failure: false)
|
49
|
+
status = RSpec::Core::Runner.run([]).to_i
|
50
|
+
|
51
|
+
exit(status) if exit_on_finish || (exit_on_failure && status != 0)
|
52
|
+
|
53
|
+
status
|
54
|
+
end
|
55
|
+
|
56
|
+
##########################################################################
|
57
|
+
|
58
|
+
#
|
59
|
+
# Defines RSpec examples for a specific forge
|
60
|
+
# Creates the test structure for a single forge file
|
61
|
+
#
|
62
|
+
# @param forge [Forge] The forge to define
|
63
|
+
#
|
64
|
+
def describe(forge)
|
65
|
+
# This is just like writing a normal RSpec test, except with loops ;)
|
66
|
+
RSpec.describe(forge.name) do
|
67
|
+
# Callback for the file
|
68
|
+
before(:context) { Callbacks.before_file(forge) }
|
69
|
+
after(:context) { Callbacks.after_file(forge) }
|
70
|
+
|
71
|
+
# Specs
|
72
|
+
forge.specs.each do |spec|
|
73
|
+
# Describe the spec
|
74
|
+
describe(spec.name) do
|
75
|
+
# Request data is for the spec and contains the base and overlays
|
76
|
+
let!(:request_data) { forge.request[spec.id] }
|
77
|
+
|
78
|
+
# The HTTP client for the spec
|
79
|
+
let!(:http_client) { HTTP::Client.new(**request_data[:base]) }
|
80
|
+
|
81
|
+
# Callback for the spec
|
82
|
+
before(:context) { Callbacks.before_spec(forge, spec) }
|
83
|
+
after(:context) { Callbacks.after_spec(forge, spec) }
|
84
|
+
|
85
|
+
# Expectations
|
86
|
+
spec.expectations.each do |expectation|
|
87
|
+
# Onto the actual expectation itself
|
88
|
+
describe(expectation.name) do
|
89
|
+
# Set metadata for the example group for error reporting
|
90
|
+
Metadata.set_for_group(spec, expectation, self)
|
91
|
+
|
92
|
+
# Lazily load the constraints
|
93
|
+
let(:constraints) { expectation.constraints.as_matchers }
|
94
|
+
|
95
|
+
let(:match_status) { constraints[:status] }
|
96
|
+
let(:match_json) { constraints[:json] }
|
97
|
+
let(:match_json_class) { be_kind_of(match_json.class) }
|
98
|
+
let(:match_headers) { constraints[:headers] }
|
99
|
+
|
100
|
+
# The request for the test itself. Overlays the expectation's data if it exists
|
101
|
+
let(:request) do
|
102
|
+
request = request_data[:base]
|
103
|
+
|
104
|
+
if (overlay = request_data[:overlay][expectation.id])
|
105
|
+
request = request.deep_merge(overlay)
|
106
|
+
end
|
107
|
+
|
108
|
+
HTTP::Request.new(**request)
|
109
|
+
end
|
110
|
+
|
111
|
+
# The Faraday response
|
112
|
+
subject(:response) { http_client.call(request) }
|
113
|
+
|
114
|
+
# Callbacks for the expectation
|
115
|
+
before :each do
|
116
|
+
Callbacks.before_expectation(
|
117
|
+
forge, spec, expectation, self, RSpec.current_example
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
# The 'after_expectation' callback is handled by Listener due to RSpec not
|
122
|
+
# reporting the example's status until after the describe block has finished.
|
123
|
+
after :each do
|
124
|
+
# However, the downside about having the callback triggered later is that RSpec
|
125
|
+
# will have reset the memoized let variables back to nil.
|
126
|
+
# This causes an issue when an expectation goes to store the state, it will end
|
127
|
+
# up re-calling the various variables and triggering another HTTP request.
|
128
|
+
# Since the variables are still memoized in this hook, it is the perfect
|
129
|
+
# time to store the referenced to them.
|
130
|
+
State.set(response:)
|
131
|
+
end
|
132
|
+
|
133
|
+
# The test itself
|
134
|
+
it(expectation.constraints.description) do
|
135
|
+
# Debugging
|
136
|
+
if spec.debug? || expectation.debug?
|
137
|
+
Callbacks.on_debug(forge, spec, expectation, self)
|
138
|
+
end
|
139
|
+
|
140
|
+
############################################################
|
141
|
+
# Status check
|
142
|
+
expect(response.status).to match_status
|
143
|
+
|
144
|
+
############################################################
|
145
|
+
# Headers check
|
146
|
+
if match_headers.present?
|
147
|
+
match_headers.each do |key, matcher|
|
148
|
+
expect(response.headers).to include(key.downcase => matcher)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
############################################################
|
153
|
+
# JSON check
|
154
|
+
if match_json.present?
|
155
|
+
expect(response.body).to match_json_class
|
156
|
+
|
157
|
+
case match_json
|
158
|
+
when Hash
|
159
|
+
match_json.each do |key, matcher|
|
160
|
+
expect(response.body).to include(key)
|
161
|
+
|
162
|
+
begin
|
163
|
+
expect(response.body[key]).to matcher
|
164
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
165
|
+
# Add the key that failed to the front of the error message
|
166
|
+
e.message.insert(0, "Key: #{key.in_quotes}\n")
|
167
|
+
raise e
|
168
|
+
end
|
169
|
+
end
|
170
|
+
else
|
171
|
+
expect(response.body).to match_json
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Runner
|
5
|
+
#
|
6
|
+
# Manages lifecycle hooks for test execution
|
7
|
+
#
|
8
|
+
# This class provides callback methods that run at specific points during test execution
|
9
|
+
# to prepare the test environment, manage state, and perform cleanup operations. These
|
10
|
+
# callbacks integrate with RSpec's test lifecycle and maintain the SpecForge context.
|
11
|
+
#
|
12
|
+
# @example Running before file callback
|
13
|
+
# Callbacks.before_file(forge)
|
14
|
+
#
|
15
|
+
class Callbacks
|
16
|
+
class << self
|
17
|
+
#
|
18
|
+
# Callback executed before a file's specs are run
|
19
|
+
#
|
20
|
+
# Initializes global context and sets up any file-level state needed
|
21
|
+
# for all specs in the file.
|
22
|
+
#
|
23
|
+
# @param forge [SpecForge::Forge] The forge representing the current file
|
24
|
+
#
|
25
|
+
def before_file(forge)
|
26
|
+
# Set the global variables
|
27
|
+
SpecForge.context.global.set(**forge.global)
|
28
|
+
|
29
|
+
# Clear the store for this file
|
30
|
+
SpecForge.context.store.clear
|
31
|
+
|
32
|
+
# Start fresh
|
33
|
+
State.clear
|
34
|
+
|
35
|
+
# Run the user's before_file callbacks
|
36
|
+
run_user_callbacks(:before_file, file_context(forge))
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Callback executed before each spec is run
|
41
|
+
#
|
42
|
+
# Prepares the context for a specific spec, including loading
|
43
|
+
# spec-level variables and configuration.
|
44
|
+
#
|
45
|
+
# @param forge [SpecForge::Forge] The forge being tested
|
46
|
+
# @param spec [SpecForge::Spec] The spec about to be executed
|
47
|
+
#
|
48
|
+
def before_spec(forge, spec)
|
49
|
+
# Prepare the variables for this spec
|
50
|
+
SpecForge.context.variables.set(**forge.variables_for_spec(spec))
|
51
|
+
|
52
|
+
# Clear any "spec" level stored data
|
53
|
+
SpecForge.context.store.clear_specs
|
54
|
+
|
55
|
+
# Run the user's before_spec callbacks
|
56
|
+
run_user_callbacks(:before_spec, spec_context(forge, spec))
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Callback executed before each expectation is run
|
61
|
+
#
|
62
|
+
# Prepares variables for the specific expectation and sets up
|
63
|
+
# example metadata for error reporting.
|
64
|
+
#
|
65
|
+
# @param forge [SpecForge::Forge] The forge being tested
|
66
|
+
# @param spec [SpecForge::Spec] The spec being tested
|
67
|
+
# @param expectation [SpecForge::Spec::Expectation] The expectation about to be evaluated
|
68
|
+
# @param example_group [RSpec::Core::ExampleGroup] The current running example group
|
69
|
+
# @param example [RSpec::Core::Example] The current example
|
70
|
+
#
|
71
|
+
def before_expectation(forge, spec, expectation, example_group, example)
|
72
|
+
# Store metadata to failure/error messages display the correct information
|
73
|
+
Metadata.set_for_example(spec, expectation)
|
74
|
+
|
75
|
+
# Store state data for callbacks and persisting data into the store
|
76
|
+
State.set(
|
77
|
+
forge:, spec:, expectation:, example_group:, example:,
|
78
|
+
request: example_group.request
|
79
|
+
)
|
80
|
+
|
81
|
+
# Load the variable overlay for this expectation (if one exists)
|
82
|
+
SpecForge.context.variables.use_overlay(expectation.id)
|
83
|
+
|
84
|
+
# Run the user's before_each callbacks
|
85
|
+
run_user_callbacks(:before_each, expectation_context(forge, spec, expectation, example))
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Handles debug mode for an expectation
|
90
|
+
#
|
91
|
+
# When debugging is enabled for a spec or expectation, this method
|
92
|
+
# creates a debugging environment for inspecting test state.
|
93
|
+
#
|
94
|
+
# @param forge [SpecForge::Forge] The forge being tested
|
95
|
+
# @param spec [SpecForge::Spec] The spec being tested
|
96
|
+
# @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
|
97
|
+
# @param example_group [RSpec::Core::ExampleGroup] The current running example group
|
98
|
+
#
|
99
|
+
def on_debug(forge, spec, expectation, example_group)
|
100
|
+
DebugProxy.new(forge, spec, expectation, example_group).call
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Callback executed after each expectation is run
|
105
|
+
#
|
106
|
+
# Performs cleanup and stores results if needed for future reference.
|
107
|
+
#
|
108
|
+
# @param forge [SpecForge::Forge] The forge being tested
|
109
|
+
# @param spec [SpecForge::Spec] The spec being tested
|
110
|
+
# @param expectation [SpecForge::Spec::Expectation] The expectation that was evaluated
|
111
|
+
# @param example_group [RSpec::Core::ExampleGroup] The current running example group
|
112
|
+
# @param example [RSpec::Core::Example] The current example
|
113
|
+
#
|
114
|
+
def after_expectation(forge, spec, expectation, example_group, example)
|
115
|
+
# Note: Let variables on `example_group` have been reset by RSpec at this point.
|
116
|
+
# Calling them will result in a new value being returned and memoized.
|
117
|
+
# In other words, do not call `example_group.response` in here unless you
|
118
|
+
# like potentially duplicating data ;)
|
119
|
+
State.persist
|
120
|
+
|
121
|
+
# Run the user's after_each callbacks
|
122
|
+
run_user_callbacks(:after_each, expectation_context(forge, spec, expectation, example))
|
123
|
+
|
124
|
+
# Clear the state for the next expectation
|
125
|
+
State.clear
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Callback executed after each spec is ran
|
130
|
+
#
|
131
|
+
# @param forge [SpecForge::Forge] The forge being tested
|
132
|
+
# @param spec [SpecForge::Spec] The spec that was executed
|
133
|
+
#
|
134
|
+
def after_spec(forge, spec)
|
135
|
+
# Run the user's after_spec callbacks
|
136
|
+
run_user_callbacks(:after_spec, spec_context(forge, spec))
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Callback executed after a file's specs have been ran
|
141
|
+
#
|
142
|
+
# @param forge [SpecForge::Forge] The forge representing the current file
|
143
|
+
#
|
144
|
+
def after_file(forge)
|
145
|
+
# Run the user's after_file callbacks
|
146
|
+
run_user_callbacks(:after_file, file_context(forge))
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
#
|
152
|
+
# Executes user-defined callbacks for a specific lifecycle point
|
153
|
+
#
|
154
|
+
# Processes the callback_type to extract timing and scope information,
|
155
|
+
# adds this metadata to the context, and then triggers all registered
|
156
|
+
# callbacks for that type.
|
157
|
+
#
|
158
|
+
# @param callback_type [Symbol, String] The type of callback to run
|
159
|
+
# (:before_file, :after_spec, etc.)
|
160
|
+
# @param context [Hash] Context data containing state information for the callback
|
161
|
+
#
|
162
|
+
# @private
|
163
|
+
#
|
164
|
+
def run_user_callbacks(callback_type, context)
|
165
|
+
callback_timing, callback_scope = callback_type.to_s.split("_")
|
166
|
+
|
167
|
+
# Adds "before_each", "before", and "each" into the context so callbacks
|
168
|
+
# can build logic off of them
|
169
|
+
context.merge!(
|
170
|
+
callback_type: callback_type.to_s,
|
171
|
+
callback_timing:, callback_scope:
|
172
|
+
)
|
173
|
+
|
174
|
+
# Run the callbacks for this type
|
175
|
+
SpecForge.context.global.callbacks.run(callback_type, context)
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Builds the base context for file-level callbacks
|
180
|
+
#
|
181
|
+
# @param forge [SpecForge::Forge] The forge representing the file
|
182
|
+
#
|
183
|
+
# @return [Hash] Basic file context
|
184
|
+
#
|
185
|
+
# @private
|
186
|
+
#
|
187
|
+
def file_context(forge)
|
188
|
+
{
|
189
|
+
forge: forge,
|
190
|
+
file_path: forge.metadata[:file_path],
|
191
|
+
file_name: forge.metadata[:file_name]
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
#
|
196
|
+
# Builds context for spec-level callbacks
|
197
|
+
# Includes file context plus spec information
|
198
|
+
#
|
199
|
+
# @param forge [SpecForge::Forge] The forge representing the file
|
200
|
+
# @param spec [SpecForge::Spec] The spec being executed
|
201
|
+
#
|
202
|
+
# @return [Hash] Context with file and spec information
|
203
|
+
#
|
204
|
+
# @private
|
205
|
+
#
|
206
|
+
def spec_context(forge, spec)
|
207
|
+
file_context(forge).merge(
|
208
|
+
spec: spec,
|
209
|
+
spec_name: spec.name,
|
210
|
+
variables: SpecForge.context.variables
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Builds context for expectation-level callbacks
|
216
|
+
# Includes spec context plus expectation information
|
217
|
+
#
|
218
|
+
# @param forge [SpecForge::Forge] The forge being tested
|
219
|
+
# @param spec [SpecForge::Spec] The spec being tested
|
220
|
+
# @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
|
221
|
+
# @param example [RSpec::Core::Example] The current example
|
222
|
+
#
|
223
|
+
# @return [Hash] Context with file, spec and expectation information
|
224
|
+
#
|
225
|
+
# @private
|
226
|
+
#
|
227
|
+
def expectation_context(forge, spec, expectation, example)
|
228
|
+
example_group = State.current.example_group
|
229
|
+
|
230
|
+
# Pull this data from the State instead of example group to avoid creating a new value
|
231
|
+
request = State.current.request
|
232
|
+
response = State.current.response
|
233
|
+
|
234
|
+
spec_context(forge, spec).merge(
|
235
|
+
expectation:,
|
236
|
+
expectation_name: expectation.name,
|
237
|
+
request:,
|
238
|
+
response:,
|
239
|
+
example_group:,
|
240
|
+
example:
|
241
|
+
)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|