spec_forge 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +4 -0
- data/CHANGELOG.md +145 -1
- data/README.md +49 -638
- 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 +141 -12
- data/lib/spec_forge/attribute/faker.rb +64 -15
- 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 +188 -13
- data/lib/spec_forge/attribute/parameterized.rb +45 -20
- 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 +168 -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 -25
- 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 +24 -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 +22 -9
- 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 +32 -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 +133 -119
- data/lib/spec_forge/spec/expectation/constraint.rb +95 -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 -37
- data/spec_forge/specs/users.yml +0 -65
data/lib/spec_forge/error.rb
CHANGED
@@ -1,150 +1,304 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
-
# Pass into to_sentence
|
5
|
-
OR_CONNECTOR = {
|
6
|
-
last_word_connector: ", or ",
|
7
|
-
two_words_connector: " or ",
|
8
|
-
# This is a minor performance improvement to avoid locales being loaded
|
9
|
-
# This will need to be removed if locales are added
|
10
|
-
locale: false
|
11
|
-
}.freeze
|
12
|
-
|
13
|
-
private_constant :OR_CONNECTOR
|
14
|
-
|
15
|
-
class Error < StandardError; end
|
16
|
-
|
17
4
|
#
|
18
|
-
#
|
5
|
+
# Base error class for all SpecForge-specific exceptions
|
19
6
|
#
|
20
|
-
class
|
21
|
-
|
22
|
-
|
23
|
-
|
7
|
+
class Error < StandardError
|
8
|
+
# Pass into to_sentence
|
9
|
+
OR_CONNECTOR = {
|
10
|
+
last_word_connector: ", or ",
|
11
|
+
two_words_connector: " or ",
|
12
|
+
# This is a minor performance improvement to avoid locales being loaded
|
13
|
+
# This will need to be removed if locales are added
|
14
|
+
locale: false
|
15
|
+
}.freeze
|
24
16
|
|
25
|
-
|
26
|
-
corrections = CLASS_CHECKER.correct(input)
|
17
|
+
private_constant :OR_CONNECTOR
|
27
18
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
19
|
+
#
|
20
|
+
# Raised when a provided Faker class name doesn't exist
|
21
|
+
# Provides helpful suggestions for similar class names
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# Attribute::Faker.new("faker.invalid.method")
|
25
|
+
# # => InvalidFakerClassError: Undefined Faker class "invalid". Did you mean? name, games, ...
|
26
|
+
#
|
27
|
+
class InvalidFakerClassError < Error
|
28
|
+
#
|
29
|
+
# A spell checker for Faker classes
|
30
|
+
#
|
31
|
+
# @return [DidYouMean::SpellChecker]
|
32
|
+
#
|
33
|
+
CLASS_CHECKER = DidYouMean::SpellChecker.new(
|
34
|
+
dictionary: Faker::Base.descendants.map { |c| c.to_s.downcase.gsub!("::", ".") }
|
33
35
|
)
|
36
|
+
|
37
|
+
def initialize(input)
|
38
|
+
corrections = CLASS_CHECKER.correct(input)
|
39
|
+
|
40
|
+
super(<<~STRING.chomp
|
41
|
+
Undefined Faker class "#{input}". #{DidYouMean::Formatter.message_for(corrections)}
|
42
|
+
|
43
|
+
For available classes, please check https://github.com/faker-ruby/faker#generators.
|
44
|
+
STRING
|
45
|
+
)
|
46
|
+
end
|
34
47
|
end
|
35
|
-
end
|
36
48
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
49
|
+
#
|
50
|
+
# Raised when a provided method for a Faker class doesn't exist
|
51
|
+
# Provides helpful suggestions for similar method names
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# Attribute::Faker.new("faker.name.invlaid")
|
55
|
+
# # => InvalidFakerMethodError: Undefined Faker method "invlaid" for "Faker::Name".
|
56
|
+
# Did you mean? first_name, last_name, ...
|
57
|
+
#
|
58
|
+
class InvalidFakerMethodError < Error
|
59
|
+
def initialize(input, klass)
|
60
|
+
spell_checker = DidYouMean::SpellChecker.new(dictionary: klass.public_methods)
|
61
|
+
corrections = spell_checker.correct(input)
|
44
62
|
|
45
|
-
|
46
|
-
|
63
|
+
super(<<~STRING.chomp
|
64
|
+
Undefined Faker method "#{input}" for "#{klass}". #{DidYouMean::Formatter.message_for(corrections)}
|
47
65
|
|
48
|
-
|
49
|
-
|
50
|
-
|
66
|
+
For available methods for this class, please check https://github.com/faker-ruby/faker#generators.
|
67
|
+
STRING
|
68
|
+
)
|
69
|
+
end
|
51
70
|
end
|
52
|
-
end
|
53
71
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
72
|
+
#
|
73
|
+
# Raised when an unknown transform function is referenced
|
74
|
+
# Indicates when a transform name isn't supported
|
75
|
+
#
|
76
|
+
class InvalidTransformFunctionError < Error
|
77
|
+
def initialize(input)
|
78
|
+
# TODO: Update link to docs
|
79
|
+
super(<<~STRING.chomp
|
80
|
+
Undefined transform function "#{input}".
|
81
|
+
|
82
|
+
For available functions, please check https://github.com/itsthedevman/spec_forge.
|
83
|
+
STRING
|
84
|
+
)
|
85
|
+
end
|
66
86
|
end
|
67
|
-
end
|
68
87
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
88
|
+
#
|
89
|
+
# Raised when a step in an invocation chain is invalid
|
90
|
+
# Provides detailed information about where in the chain the error occurred
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# variable_attr = Attribute::Variable.new("variables.user.invalid_method")
|
94
|
+
# variable_attr.resolved
|
95
|
+
# # => InvalidInvocationError: Cannot invoke "invalid_method" on User
|
96
|
+
#
|
97
|
+
class InvalidInvocationError < Error
|
98
|
+
def initialize(step, object, resolution_path = {})
|
99
|
+
@step = step
|
100
|
+
@object = object
|
101
|
+
@resolution_path = resolution_path
|
102
|
+
|
103
|
+
object_class =
|
104
|
+
case object
|
105
|
+
when Data
|
106
|
+
object.class.name || "Data"
|
107
|
+
when Struct
|
108
|
+
object.class.name || "Struct"
|
109
|
+
else
|
110
|
+
object.class
|
111
|
+
end
|
112
|
+
|
113
|
+
super(<<~STRING.chomp
|
114
|
+
Cannot invoke "#{step}" on #{object_class}
|
115
|
+
#{resolution_path_message}
|
116
|
+
STRING
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Creates a new InvalidInvocationError with a new resolution path
|
122
|
+
#
|
123
|
+
# @param path [Hash] The steps taken up until this point
|
124
|
+
#
|
125
|
+
def with_resolution_path(path)
|
126
|
+
self.class.new(@step, @object, path)
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def resolution_path_message
|
132
|
+
return "" if @resolution_path.empty?
|
133
|
+
|
134
|
+
message =
|
135
|
+
@resolution_path.map.with_index do |(path, description), index|
|
136
|
+
"#{index + 1}. #{path} --> #{description}"
|
137
|
+
end.join("\n")
|
138
|
+
|
139
|
+
"\nResolution path:\n#{message}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# An extended version of TypeError with better error messages
|
145
|
+
# Makes it easier to understand type mismatches in the codebase
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# raise Error::InvalidTypeError.new(123, String, for: "name parameter")
|
149
|
+
# # => Expected String, got Integer for name parameter
|
150
|
+
#
|
151
|
+
class InvalidTypeError < Error
|
152
|
+
def initialize(object, expected_type, **opts)
|
153
|
+
if expected_type.instance_of?(Array)
|
154
|
+
expected_type = expected_type.to_sentence(**OR_CONNECTOR)
|
82
155
|
end
|
83
156
|
|
84
|
-
|
85
|
-
|
157
|
+
message = "Expected #{expected_type}, got #{object.class}"
|
158
|
+
message += " for #{opts[:for]}" if opts[:for].present?
|
86
159
|
|
87
|
-
|
88
|
-
|
89
|
-
)
|
160
|
+
super(message)
|
161
|
+
end
|
90
162
|
end
|
91
|
-
end
|
92
163
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
164
|
+
#
|
165
|
+
# Raised when a variable reference cannot be resolved
|
166
|
+
# Indicates when a spec or expectation references an undefined variable
|
167
|
+
#
|
168
|
+
class MissingVariableError < Error
|
169
|
+
def initialize(variable_name)
|
170
|
+
super("Undefined variable \"#{variable_name}\" referenced in expectation")
|
100
171
|
end
|
172
|
+
end
|
101
173
|
|
102
|
-
|
103
|
-
|
174
|
+
#
|
175
|
+
# Raised when a YAML structure doesn't match expectations
|
176
|
+
# Acts as a container for multiple validation errors
|
177
|
+
#
|
178
|
+
class InvalidStructureError < Error
|
179
|
+
def initialize(errors)
|
180
|
+
message = errors.to_a.join_map("\n") do |error|
|
181
|
+
next error if error.is_a?(SpecForge::Error)
|
104
182
|
|
105
|
-
|
183
|
+
# Normal errors, let's get verbose
|
184
|
+
backtrace = SpecForge.backtrace_cleaner.clean(error.backtrace)
|
185
|
+
"#{error.inspect}\n # ./#{backtrace.join("\n # ./")}\n"
|
186
|
+
end
|
187
|
+
|
188
|
+
super(message)
|
189
|
+
end
|
106
190
|
end
|
107
|
-
end
|
108
191
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
192
|
+
#
|
193
|
+
# Raised when an unknown factory build strategy is provided
|
194
|
+
# Indicates when a strategy string doesn't match supported options
|
195
|
+
#
|
196
|
+
class InvalidBuildStrategy < Error
|
197
|
+
def initialize(build_strategy)
|
198
|
+
valid_strategies = Attribute::Factory::BUILD_STRATEGIES.to_sentence(**OR_CONNECTOR)
|
199
|
+
|
200
|
+
super(<<~STRING.chomp
|
201
|
+
Unknown build strategy "#{build_strategy}" referenced in spec.
|
202
|
+
|
203
|
+
Valid strategies include: #{valid_strategies}
|
204
|
+
STRING
|
205
|
+
)
|
206
|
+
end
|
115
207
|
end
|
116
|
-
end
|
117
208
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
209
|
+
#
|
210
|
+
# Raised when a spec file cannot be loaded
|
211
|
+
# Provides detailed information about the cause of the loading error
|
212
|
+
#
|
213
|
+
class SpecLoadError < Error
|
214
|
+
def initialize(error, file_path, spec: nil)
|
215
|
+
message =
|
216
|
+
if spec
|
217
|
+
"Error loading spec #{spec[:name].in_quotes} in file #{file_path.in_quotes} (line #{spec[:line_number]})"
|
218
|
+
else
|
219
|
+
"Error loading spec file #{file_path.in_quotes}"
|
220
|
+
end
|
221
|
+
|
222
|
+
causes = error.message.split("\n").map(&:strip).reject(&:empty?)
|
223
|
+
|
224
|
+
message +=
|
225
|
+
if causes.size > 1
|
226
|
+
"\nCauses:\n - #{causes.join_map("\n - ")}"
|
227
|
+
else
|
228
|
+
"\nCause: #{error}"
|
229
|
+
end
|
230
|
+
|
231
|
+
super(message)
|
129
232
|
end
|
233
|
+
end
|
130
234
|
|
131
|
-
|
235
|
+
#
|
236
|
+
# Raised when the provided namespace is not defined on the global context
|
237
|
+
#
|
238
|
+
class InvalidGlobalNamespaceError < Error
|
239
|
+
def initialize(provided_namespace)
|
240
|
+
super("Invalid global namespace #{provided_namespace.in_quotes}. Currently supported namespaces are: \"variables\"")
|
241
|
+
end
|
132
242
|
end
|
133
|
-
end
|
134
243
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
244
|
+
#
|
245
|
+
# Raised when the provided matcher name does not defined with RSpec
|
246
|
+
#
|
247
|
+
class UndefinedMatcherError < Error
|
248
|
+
def initialize(matcher_name)
|
249
|
+
matcher_categories = {
|
250
|
+
Equality: ["matcher.eq", "matcher.eql", "matcher.equal"],
|
251
|
+
Types: ["kind_of.string", "kind_of.integer", "kind_of.array", "kind_of.hash"],
|
252
|
+
Truthiness: ["be.true", "be.false", "be.nil"],
|
253
|
+
Comparison: ["be.within", "be.between", "be.greater_than", "be.less_than"],
|
254
|
+
Collections: ["matcher.include", "matcher.contain_exactly", "matcher.all"],
|
255
|
+
Strings: ["/regex/", "matcher.start_with", "matcher.end_with"]
|
256
|
+
}
|
141
257
|
|
142
|
-
|
143
|
-
|
258
|
+
formatted_categories =
|
259
|
+
matcher_categories.join_map("\n") do |category, matchers|
|
260
|
+
" #{category}: #{matchers.join(", ")}"
|
261
|
+
end
|
144
262
|
|
145
|
-
|
146
|
-
|
147
|
-
|
263
|
+
super(<<~STRING.chomp
|
264
|
+
Undefined matcher method "#{matcher_name}" is not available in RSpec matchers.
|
265
|
+
|
266
|
+
Common matchers you can use:
|
267
|
+
#{formatted_categories}
|
268
|
+
|
269
|
+
For the complete list of available matchers, check the RSpec documentation:
|
270
|
+
https://rspec.info/documentation/3.12/rspec-expectations/RSpec/Matchers.html
|
271
|
+
STRING
|
272
|
+
)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# Raised when a callback is referenced in config but hasn't been defined
|
278
|
+
#
|
279
|
+
class UndefinedCallbackError < Error
|
280
|
+
def initialize(callback_name, available_callbacks = [])
|
281
|
+
message = "The callback #{callback_name.in_quotes} was referenced but hasn't been defined."
|
282
|
+
|
283
|
+
message +=
|
284
|
+
if available_callbacks.any?
|
285
|
+
<<~STR.chomp
|
286
|
+
|
287
|
+
Available callbacks are: #{available_callbacks.join_map(", ", &:in_quotes)}
|
288
|
+
STR
|
289
|
+
else
|
290
|
+
<<~STR.chomp
|
291
|
+
|
292
|
+
No callbacks have been defined yet. Register callbacks with:
|
293
|
+
|
294
|
+
SpecForge.register_callback(:#{callback_name}) do |context|
|
295
|
+
# Your callback code
|
296
|
+
end
|
297
|
+
STR
|
298
|
+
end
|
299
|
+
|
300
|
+
super(message)
|
301
|
+
end
|
148
302
|
end
|
149
303
|
end
|
150
304
|
end
|
data/lib/spec_forge/factory.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
+
#
|
5
|
+
# Manages factory definitions and registration with FactoryBot
|
6
|
+
# Provides methods for loading factories from YAML files
|
7
|
+
#
|
4
8
|
class Factory
|
5
9
|
#
|
6
|
-
# Loads
|
10
|
+
# Loads factories from files and registers them with FactoryBot
|
11
|
+
# Sets up paths and loads definitions based on configuration
|
7
12
|
#
|
8
13
|
def self.load_and_register
|
9
14
|
if SpecForge.configuration.factories.paths?
|
@@ -17,14 +22,13 @@ module SpecForge
|
|
17
22
|
end
|
18
23
|
|
19
24
|
#
|
20
|
-
# Loads
|
25
|
+
# Loads factory definitions from YAML files
|
26
|
+
# Creates Factory instances but doesn't register them with FactoryBot
|
21
27
|
#
|
22
|
-
# @return [Array<Factory>]
|
23
|
-
# Note: This factories have not been registered with FactoryBot.
|
24
|
-
# See #register
|
28
|
+
# @return [Array<Factory>] Array of loaded factory instances
|
25
29
|
#
|
26
30
|
def self.load_from_files
|
27
|
-
path = SpecForge.
|
31
|
+
path = SpecForge.forge_path.join("factories", "**/*.yml")
|
28
32
|
|
29
33
|
factories = []
|
30
34
|
|
@@ -43,13 +47,28 @@ module SpecForge
|
|
43
47
|
|
44
48
|
############################################################################
|
45
49
|
|
46
|
-
|
50
|
+
# @return [Symbol, String] The name of the factory
|
51
|
+
attr_reader :name
|
47
52
|
|
53
|
+
# @return [Hash] The raw input that defined this factory
|
54
|
+
attr_reader :input
|
55
|
+
|
56
|
+
# @return [String, nil] The model class name this factory represents, if specified
|
57
|
+
attr_reader :model_class
|
58
|
+
|
59
|
+
# @return [Hash<Symbol, Attribute>] Variables defined for this factory
|
60
|
+
attr_reader :variables
|
61
|
+
|
62
|
+
# @return [Hash<Symbol, Attribute>] The attributes that define this factory
|
63
|
+
attr_reader :attributes
|
64
|
+
|
65
|
+
#
|
66
|
+
# Creates a new Factory instance
|
48
67
|
#
|
49
|
-
#
|
68
|
+
# @param name [String, Symbol] The name of the factory
|
69
|
+
# @param input [Hash] The attributes defining the factory
|
50
70
|
#
|
51
|
-
# @
|
52
|
-
# @param **input [Hash] Attributes to define the factory. See Normalizer::Factory
|
71
|
+
# @return [Factory] A new factory instance
|
53
72
|
#
|
54
73
|
def initialize(name:, **input)
|
55
74
|
@name = name
|
@@ -63,10 +82,10 @@ module SpecForge
|
|
63
82
|
end
|
64
83
|
|
65
84
|
#
|
66
|
-
# Registers this factory with FactoryBot
|
67
|
-
#
|
85
|
+
# Registers this factory with FactoryBot
|
86
|
+
# Makes the factory available for use in specs
|
68
87
|
#
|
69
|
-
# @return [
|
88
|
+
# @return [self] Returns self for method chaining
|
70
89
|
#
|
71
90
|
def register
|
72
91
|
dsl = FactoryBot::Syntax::Default::DSL.new
|
@@ -78,7 +97,7 @@ module SpecForge
|
|
78
97
|
factory_forge = self
|
79
98
|
dsl.factory(name, options) do
|
80
99
|
factory_forge.attributes.each do |name, attribute|
|
81
|
-
add_attribute(name) { attribute.
|
100
|
+
add_attribute(name) { attribute.resolve }
|
82
101
|
end
|
83
102
|
end
|
84
103
|
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Provides filtering capabilities for test suites based on different criteria
|
6
|
+
#
|
7
|
+
# The Filter class allows running specific tests by filtering forges, specs,
|
8
|
+
# and expectations based on file name, spec name, and expectation name.
|
9
|
+
#
|
10
|
+
# @example Filtering specs by name
|
11
|
+
# forges = Loader.load_from_files
|
12
|
+
# filtered = Filter.apply(forges, file_name: "users", spec_name: "create_user")
|
13
|
+
#
|
14
|
+
class Filter
|
15
|
+
class << self
|
16
|
+
#
|
17
|
+
# Prints out a message if any of the filters were used
|
18
|
+
#
|
19
|
+
# @param forges [Array<Forge>] The collection of forges that was filtered
|
20
|
+
# @param file_name [String, nil] Optional file name that was used by the filter
|
21
|
+
# @param spec_name [String, nil] Optional spec name that was used by the filter
|
22
|
+
# @param expectation_name [String, nil] Optional expectation name that was used by the filter
|
23
|
+
#
|
24
|
+
def announce(forges, file_name:, spec_name:, expectation_name:)
|
25
|
+
filters = {file_name:, spec_name:, expectation_name:}.reject { |k, v| v.blank? }
|
26
|
+
return if filters.size == 0
|
27
|
+
|
28
|
+
filters_display = filters.join_map(", ") { |k, v| "#{k.in_quotes} => #{v.in_quotes}" }
|
29
|
+
|
30
|
+
expectation_count = forges.sum do |forge|
|
31
|
+
forge.specs.sum { |spec| spec.expectations.size }
|
32
|
+
end
|
33
|
+
|
34
|
+
puts "Applied filter #{filters_display}"
|
35
|
+
puts "Found #{expectation_count} #{"expectation".pluralize(expectation_count)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Filters a collection of forges based on specified criteria
|
40
|
+
#
|
41
|
+
# This method allows running specific tests by filtering forges, specs,
|
42
|
+
# and expectations based on file name, spec name, and expectation name.
|
43
|
+
# It returns only the forges, specs, and expectations that match the criteria.
|
44
|
+
#
|
45
|
+
# @param forges [Array<Forge>] The collection of forges to filter
|
46
|
+
# @param file_name [String, nil] Optional file name to filter by
|
47
|
+
# @param spec_name [String, nil] Optional spec name to filter by
|
48
|
+
# @param expectation_name [String, nil] Optional expectation name to filter by
|
49
|
+
#
|
50
|
+
# @return [Array<Forge>] The filtered collection of forges
|
51
|
+
#
|
52
|
+
# @raise [ArgumentError] If filtering parameters are provided in an invalid combination
|
53
|
+
#
|
54
|
+
def apply(forges, file_name: nil, spec_name: nil, expectation_name: nil)
|
55
|
+
# Guard against invalid partial filters
|
56
|
+
if expectation_name && spec_name.blank?
|
57
|
+
raise ArgumentError, "The spec's name is required when filtering by an expectation's name"
|
58
|
+
end
|
59
|
+
|
60
|
+
if spec_name && file_name.blank?
|
61
|
+
raise ArgumentError, "The spec's filename is required when filtering by a spec's name"
|
62
|
+
end
|
63
|
+
|
64
|
+
forges.filter_map do |forge|
|
65
|
+
specs = forge.specs.filter_map do |spec|
|
66
|
+
next if file_name && spec.file_name != file_name # File filter
|
67
|
+
next if spec_name && spec.name != spec_name # Name filter
|
68
|
+
|
69
|
+
# Expectation filter
|
70
|
+
next spec unless expectation_name
|
71
|
+
|
72
|
+
expectations = spec.expectations.select { |e| e.name == expectation_name }
|
73
|
+
next if expectations.empty?
|
74
|
+
|
75
|
+
spec.expectations = expectations
|
76
|
+
spec
|
77
|
+
end
|
78
|
+
|
79
|
+
next if specs.empty?
|
80
|
+
|
81
|
+
forge.specs = specs
|
82
|
+
forge
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|