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
@@ -1,54 +1,121 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
+
#
|
5
|
+
# Base class for normalizing various data structures in SpecForge
|
6
|
+
#
|
7
|
+
# The Normalizer validates and standardizes user input from YAML files,
|
8
|
+
# ensuring it meets the expected structure and types before processing.
|
9
|
+
# It supports default values, type checking, aliases, and nested structures.
|
10
|
+
#
|
11
|
+
# @example Normalizing a hash
|
12
|
+
# normalizer = Normalizer.new("spec", input_hash)
|
13
|
+
# output, errors = normalizer.normalize
|
14
|
+
#
|
4
15
|
class Normalizer
|
16
|
+
#
|
17
|
+
# Shared attributes used by the various normalizers
|
18
|
+
#
|
19
|
+
# @return [Hash<Symbol, Hash>]
|
20
|
+
#
|
5
21
|
SHARED_ATTRIBUTES = {
|
22
|
+
id: {type: String},
|
23
|
+
name: {type: String},
|
24
|
+
line_number: {type: Integer},
|
6
25
|
base_url: {
|
7
26
|
type: String,
|
8
|
-
default:
|
27
|
+
default: nil
|
9
28
|
},
|
10
29
|
url: {
|
11
30
|
type: String,
|
12
31
|
aliases: %i[path],
|
13
|
-
default:
|
32
|
+
default: nil
|
14
33
|
},
|
15
|
-
|
34
|
+
http_verb: {
|
16
35
|
type: String,
|
17
|
-
aliases: %i[method],
|
18
|
-
default: ""
|
36
|
+
aliases: %i[method http_method],
|
37
|
+
default: nil, # Do not default this to "GET". Leave it nil. Seriously.
|
38
|
+
validator: lambda do |value|
|
39
|
+
valid_verbs = HTTP::Verb::VERBS.values
|
40
|
+
return if value.blank? || valid_verbs.include?(value.to_s.upcase)
|
41
|
+
|
42
|
+
raise Error, "Invalid HTTP verb: #{value}. Valid values are: #{valid_verbs.join(", ")}"
|
43
|
+
end
|
19
44
|
},
|
20
45
|
headers: {
|
21
46
|
type: Hash,
|
22
47
|
default: {}
|
23
48
|
},
|
24
49
|
query: {
|
25
|
-
type: Hash,
|
50
|
+
type: [Hash, String],
|
26
51
|
aliases: %i[params],
|
27
52
|
default: {}
|
28
53
|
},
|
29
54
|
body: {
|
30
|
-
type: Hash,
|
55
|
+
type: [Hash, String],
|
31
56
|
aliases: %i[data],
|
32
57
|
default: {}
|
33
58
|
},
|
34
59
|
variables: {
|
35
|
-
type: Hash,
|
60
|
+
type: [Hash, String],
|
36
61
|
default: {}
|
37
62
|
},
|
38
63
|
debug: {
|
39
64
|
type: [TrueClass, FalseClass],
|
40
65
|
default: false,
|
41
66
|
aliases: %i[pry breakpoint]
|
67
|
+
},
|
68
|
+
callback: {
|
69
|
+
type: [String, NilClass],
|
70
|
+
validator: lambda do |value|
|
71
|
+
return if value.blank?
|
72
|
+
return if SpecForge::Callbacks.registered?(value)
|
73
|
+
|
74
|
+
raise Error::UndefinedCallbackError.new(value, SpecForge::Callbacks.registered_names)
|
75
|
+
end
|
42
76
|
}
|
43
77
|
}.freeze
|
44
78
|
|
79
|
+
#
|
80
|
+
# Defines the normalized structure for validating and parsing input data
|
81
|
+
#
|
82
|
+
# Each key represents an attribute with its validation and transformation rules.
|
83
|
+
# The structure supports defining:
|
84
|
+
# - Expected data type(s)
|
85
|
+
# - Default values
|
86
|
+
# - Aliases for alternative key names
|
87
|
+
# - Optional validation logic
|
88
|
+
# - Nested sub-structures
|
89
|
+
#
|
90
|
+
# @return [Hash] A configuration hash defining attribute validation rules
|
91
|
+
#
|
92
|
+
# @example Basic structure definition
|
93
|
+
# STRUCTURE = {
|
94
|
+
# name: {
|
95
|
+
# type: String, # Must be a String
|
96
|
+
# default: "", # Default to empty string if not provided
|
97
|
+
# aliases: [:title] # Allows using 'title' as an alternative key
|
98
|
+
# },
|
99
|
+
# age: {
|
100
|
+
# type: Integer, # Must be an Integer
|
101
|
+
# default: 0 # Default to 0 if not provided
|
102
|
+
# }
|
103
|
+
# }
|
104
|
+
#
|
105
|
+
# @see Normalizer
|
106
|
+
#
|
45
107
|
STRUCTURE = {}
|
46
108
|
|
47
109
|
class << self
|
48
110
|
#
|
49
111
|
# Raises any errors collected by the block
|
50
112
|
#
|
51
|
-
# @
|
113
|
+
# @yield Block that returns [output, errors]
|
114
|
+
# @yieldreturn [Array<Object, Set>] The result and any errors
|
115
|
+
#
|
116
|
+
# @return [Object] The normalized output if successful
|
117
|
+
#
|
118
|
+
# @raise [Error::InvalidStructureError] If any errors were encountered
|
52
119
|
#
|
53
120
|
# @private
|
54
121
|
#
|
@@ -62,7 +129,7 @@ module SpecForge
|
|
62
129
|
errors << e
|
63
130
|
end
|
64
131
|
|
65
|
-
raise InvalidStructureError.new(errors) if errors.size > 0
|
132
|
+
raise Error::InvalidStructureError.new(errors) if errors.size > 0
|
66
133
|
|
67
134
|
output
|
68
135
|
end
|
@@ -70,6 +137,8 @@ module SpecForge
|
|
70
137
|
#
|
71
138
|
# Returns a default version of this normalizer
|
72
139
|
#
|
140
|
+
# @return [Hash] Default structure with default values
|
141
|
+
#
|
73
142
|
# @private
|
74
143
|
#
|
75
144
|
def default
|
@@ -77,7 +146,14 @@ module SpecForge
|
|
77
146
|
end
|
78
147
|
end
|
79
148
|
|
80
|
-
|
149
|
+
# @return [String] A label that describes the data itself
|
150
|
+
attr_reader :label
|
151
|
+
|
152
|
+
# @return [Hash] The data to normalize
|
153
|
+
attr_reader :input
|
154
|
+
|
155
|
+
# @return [Hash] The structure to normalize the data to
|
156
|
+
attr_reader :structure
|
81
157
|
|
82
158
|
#
|
83
159
|
# Creates a normalizer for normalizing Hash data based on a structure
|
@@ -86,6 +162,8 @@ module SpecForge
|
|
86
162
|
# @param input [Hash] The data to normalize
|
87
163
|
# @param structure [Hash] The structure to normalize the data to
|
88
164
|
#
|
165
|
+
# @return [Normalizer] A new normalizer instance
|
166
|
+
#
|
89
167
|
def initialize(label, input, structure: self.class::STRUCTURE)
|
90
168
|
@label = label
|
91
169
|
@input = input
|
@@ -93,68 +171,88 @@ module SpecForge
|
|
93
171
|
end
|
94
172
|
|
95
173
|
#
|
96
|
-
# Normalizes the data
|
174
|
+
# Normalizes the data according to the defined structure
|
97
175
|
#
|
98
|
-
# @return [Hash] The normalized data
|
176
|
+
# @return [Array<Hash, Set>] The normalized data and any errors
|
99
177
|
#
|
100
178
|
def normalize
|
101
|
-
|
179
|
+
case input
|
180
|
+
when Hash
|
181
|
+
normalize_hash
|
182
|
+
when Array
|
183
|
+
normalize_array
|
184
|
+
end
|
102
185
|
end
|
103
186
|
|
104
187
|
#
|
105
188
|
# Returns a hash with the default structure
|
106
189
|
#
|
107
|
-
# @return [Hash]
|
190
|
+
# @return [Hash] A hash with default values for all structure keys
|
108
191
|
#
|
109
192
|
def default
|
110
|
-
structure.
|
111
|
-
|
112
|
-
value
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
193
|
+
structure.each_with_object({}) do |(key, value), hash|
|
194
|
+
hash[key] =
|
195
|
+
if value.key?(:default)
|
196
|
+
default = value[:default]
|
197
|
+
next if default.nil?
|
198
|
+
|
199
|
+
default.dup
|
200
|
+
elsif value[:type] == Integer # Can't call new on int
|
201
|
+
0
|
202
|
+
elsif value[:type] == Proc # Sameeee
|
203
|
+
-> {}
|
204
|
+
else
|
205
|
+
value[:type].new
|
206
|
+
end
|
120
207
|
end
|
121
208
|
end
|
122
209
|
|
123
210
|
protected
|
124
211
|
|
125
|
-
|
212
|
+
#
|
213
|
+
# Normalizes the input hash according to the structure definition
|
214
|
+
#
|
215
|
+
# @return [Array<Hash, Set>] Normalized hash and any errors
|
216
|
+
#
|
217
|
+
# @private
|
218
|
+
#
|
219
|
+
def normalize_hash
|
126
220
|
output, errors = {}, Set.new
|
127
221
|
|
128
222
|
structure.each do |key, attribute|
|
129
223
|
type_class = attribute[:type]
|
130
224
|
aliases = attribute[:aliases] || []
|
131
|
-
sub_structure = attribute[:structure]
|
132
225
|
default = attribute[:default]
|
133
|
-
|
226
|
+
|
227
|
+
has_default = attribute.key?(:default)
|
228
|
+
nilable = has_default && default.nil?
|
134
229
|
|
135
230
|
# Get the value
|
136
231
|
value = value_from_keys(input, [key] + aliases)
|
232
|
+
next if nilable && value.nil?
|
137
233
|
|
138
234
|
# Default the value if needed
|
139
|
-
value = default.dup if
|
235
|
+
value = default.dup if has_default && value.nil?
|
140
236
|
|
141
237
|
# Type + existence check
|
142
238
|
if !valid_class?(value, type_class)
|
143
|
-
|
239
|
+
for_context = generate_error_label(key, aliases)
|
240
|
+
|
241
|
+
if (line_number = input[:line_number])
|
242
|
+
for_context += " (line #{line_number})"
|
243
|
+
end
|
244
|
+
|
245
|
+
raise Error::InvalidTypeError.new(value, type_class, for: for_context)
|
144
246
|
end
|
145
247
|
|
146
|
-
|
147
|
-
|
148
|
-
when [Hash, Hash]
|
149
|
-
new_value, new_errors = self.class
|
150
|
-
.new(label, value, structure: sub_structure)
|
151
|
-
.normalize
|
248
|
+
# Call the validator if it has one
|
249
|
+
attribute[:validator]&.call(value)
|
152
250
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
251
|
+
# Normalize any sub structures
|
252
|
+
if (substructure = attribute[:structure])
|
253
|
+
new_label = generate_error_label(key, aliases)
|
254
|
+
value = normalize_substructure(new_label, value, substructure, errors)
|
255
|
+
end
|
158
256
|
|
159
257
|
# Store
|
160
258
|
output[key] = value
|
@@ -165,10 +263,30 @@ module SpecForge
|
|
165
263
|
[output, errors]
|
166
264
|
end
|
167
265
|
|
266
|
+
#
|
267
|
+
# Extracts a value from a hash checking multiple keys
|
268
|
+
#
|
269
|
+
# @param hash [Hash] The hash to extract from
|
270
|
+
# @param keys [Array<Symbol, String>] The keys to check
|
271
|
+
#
|
272
|
+
# @return [Object, nil] The value if found, nil otherwise
|
273
|
+
#
|
274
|
+
# @private
|
275
|
+
#
|
168
276
|
def value_from_keys(hash, keys)
|
169
277
|
hash.find { |k, v| keys.include?(k) }&.second
|
170
278
|
end
|
171
279
|
|
280
|
+
#
|
281
|
+
# Checks if a value is of the expected type
|
282
|
+
#
|
283
|
+
# @param value [Object] The value to check
|
284
|
+
# @param expected_type [Class, Array<Class>] The expected type(s)
|
285
|
+
#
|
286
|
+
# @return [Boolean] Whether the value is of the expected type
|
287
|
+
#
|
288
|
+
# @private
|
289
|
+
#
|
172
290
|
def valid_class?(value, expected_type)
|
173
291
|
if expected_type.instance_of?(Array)
|
174
292
|
expected_type.any? { |type| value.is_a?(type) }
|
@@ -176,6 +294,102 @@ module SpecForge
|
|
176
294
|
value.is_a?(expected_type)
|
177
295
|
end
|
178
296
|
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# Generates an error label with information about the key and its aliases
|
300
|
+
#
|
301
|
+
# Creates a descriptive label for error messages that includes the key name,
|
302
|
+
# any aliases it may have, and the context in which it appears.
|
303
|
+
#
|
304
|
+
# @param key [Symbol, String] The key that caused the error
|
305
|
+
# @param aliases [Array<Symbol, String>] Any aliases for the key
|
306
|
+
#
|
307
|
+
# @return [String] A formatted error label
|
308
|
+
#
|
309
|
+
# @example
|
310
|
+
# generate_error_label(:user_id, [:id, :uid])
|
311
|
+
# # => "\"user_id\" (aliases \"id\", \"uid\") in user config"
|
312
|
+
#
|
313
|
+
def generate_error_label(key, aliases)
|
314
|
+
error_label = key.to_s.in_quotes
|
315
|
+
|
316
|
+
if aliases.size > 0
|
317
|
+
aliases = aliases.join_map(", ") { |a| a.to_s.in_quotes }
|
318
|
+
error_label += " (aliases #{aliases})"
|
319
|
+
end
|
320
|
+
|
321
|
+
error_label + " in #{label}"
|
322
|
+
end
|
323
|
+
|
324
|
+
#
|
325
|
+
# Normalizes a nested substructure within a parent structure
|
326
|
+
#
|
327
|
+
# Recursively processes nested Hash or Array structures according to
|
328
|
+
# their structure definitions, collecting any validation errors.
|
329
|
+
#
|
330
|
+
# @param new_label [String] The label to use for error messages
|
331
|
+
# @param value [Hash, Array] The nested structure to normalize
|
332
|
+
# @param substructure [Hash] The structure definition for validation
|
333
|
+
# @param errors [Set] A set to collect any encountered errors
|
334
|
+
#
|
335
|
+
# @return [Hash, Array] The normalized substructure
|
336
|
+
#
|
337
|
+
# @example
|
338
|
+
# value = {name: "Test", age: "25"}
|
339
|
+
# substructure = {name: {type: String}, age: {type: Integer}}
|
340
|
+
# normalize_substructure("user", value, substructure, Set.new)
|
341
|
+
# # => {name: "Test", age: 25}
|
342
|
+
#
|
343
|
+
def normalize_substructure(new_label, value, substructure, errors)
|
344
|
+
return value unless value.is_a?(Hash) || value.is_a?(Array)
|
345
|
+
|
346
|
+
new_value, new_errors = self.class
|
347
|
+
.new(new_label, value, structure: substructure)
|
348
|
+
.normalize
|
349
|
+
|
350
|
+
errors.merge(new_errors) if new_errors.size > 0
|
351
|
+
new_value
|
352
|
+
end
|
353
|
+
|
354
|
+
#
|
355
|
+
# Normalizes an array according to its structure definition
|
356
|
+
#
|
357
|
+
# Processes each element in the input array, validating its type and
|
358
|
+
# recursively normalizing any nested structures.
|
359
|
+
#
|
360
|
+
# @return [Array<Object, Set>] A two-element array containing:
|
361
|
+
# 1. The normalized array
|
362
|
+
# 2. A set of any errors encountered during normalization
|
363
|
+
#
|
364
|
+
# @example
|
365
|
+
# input = [1, "string", 3]
|
366
|
+
# structure = {type: Numeric}
|
367
|
+
# normalize_array # => [[1, 3], #<Set: {Error}>]
|
368
|
+
#
|
369
|
+
def normalize_array
|
370
|
+
output, errors = [], Set.new
|
371
|
+
|
372
|
+
input.each_with_index do |value, index|
|
373
|
+
type_class = structure[:type]
|
374
|
+
|
375
|
+
if !valid_class?(value, type_class)
|
376
|
+
raise Error::InvalidTypeError.new(value, type_class, for: "index #{index} of #{label}")
|
377
|
+
end
|
378
|
+
|
379
|
+
# Call the validator if it has one
|
380
|
+
structure[:validator]&.call(value)
|
381
|
+
|
382
|
+
if (substructure = structure[:structure])
|
383
|
+
value = normalize_substructure("index #{index} of #{label}", value, substructure, errors)
|
384
|
+
end
|
385
|
+
|
386
|
+
output << value
|
387
|
+
rescue => e
|
388
|
+
errors << e
|
389
|
+
end
|
390
|
+
|
391
|
+
[output, errors]
|
392
|
+
end
|
179
393
|
end
|
180
394
|
end
|
181
395
|
|
@@ -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
|