spec_forge 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +106 -1
- data/README.md +34 -22
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +91 -14
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -22
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +21 -8
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +27 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +132 -123
- data/lib/spec_forge/spec/expectation/constraint.rb +91 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages variable resolution across different expectations in SpecForge tests.
|
7
|
+
#
|
8
|
+
# The Variables class handles two layers of variable definitions:
|
9
|
+
# - Base variables: The core set of variables defined at the spec level
|
10
|
+
# - Overlay variables: Additional variables defined at the expectation level
|
11
|
+
# that can override base variables with the same name.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# variables = Variables.new(
|
15
|
+
# base: {user_id: 123, name: "Test User"},
|
16
|
+
# overlay: {
|
17
|
+
# "expectation_1": {name: "Override User"}
|
18
|
+
# }
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
# variables[:user_id] #=> 123
|
22
|
+
# variables[:name] #=> "Test User"
|
23
|
+
#
|
24
|
+
# variables.use_overlay("expectation_1")
|
25
|
+
# variables[:name] #=> "Override User"
|
26
|
+
# variables[:user_id] #=> 123 (unchanged)
|
27
|
+
#
|
28
|
+
class Variables < Hash
|
29
|
+
attr_reader :base, :overlay
|
30
|
+
|
31
|
+
#
|
32
|
+
# Creates a new Variables container with base and overlay definitions
|
33
|
+
#
|
34
|
+
# @param base [Hash] The base set of variables (typically defined at spec level)
|
35
|
+
# @param overlay [Hash<String, Hash>] A hash of overlay variable sets keyed by ID
|
36
|
+
#
|
37
|
+
# @return [Variables]
|
38
|
+
#
|
39
|
+
def initialize(base: {}, overlay: {})
|
40
|
+
set(base:, overlay:)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Sets the base and overlay variable hashes
|
45
|
+
#
|
46
|
+
# @param base [Hash] The new base variable hash
|
47
|
+
# @param overlay [Hash<String, Hash>] The new overlay variable hashes
|
48
|
+
#
|
49
|
+
# @return [self]
|
50
|
+
#
|
51
|
+
def set(base:, overlay: {})
|
52
|
+
@base = Attribute.from(base)
|
53
|
+
@overlay = overlay
|
54
|
+
|
55
|
+
resolve_into_self(@base)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Applies a specific overlay to the base variables
|
61
|
+
# If the overlay doesn't exist or is empty, no changes are made.
|
62
|
+
#
|
63
|
+
# @param id [String] The ID of the overlay to apply
|
64
|
+
#
|
65
|
+
# @return [nil]
|
66
|
+
#
|
67
|
+
def use_overlay(id)
|
68
|
+
active = @base
|
69
|
+
|
70
|
+
if (overlay = @overlay[id]) && overlay.present?
|
71
|
+
active = active.deep_merge(overlay)
|
72
|
+
end
|
73
|
+
|
74
|
+
resolve_into_self(active)
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def resolve_into_self(hash)
|
81
|
+
# Start fresh
|
82
|
+
clear
|
83
|
+
|
84
|
+
# Load the resolved values into self
|
85
|
+
hash.each do |key, value|
|
86
|
+
self[key] = Attribute.from(value).resolved
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Core data structure that maintains context during test execution
|
6
|
+
#
|
7
|
+
# Context stores and provides access to global variables, test variables, and
|
8
|
+
# shared state across specs.
|
9
|
+
# It acts as a central repository for test data during execution.
|
10
|
+
#
|
11
|
+
# @example Accessing the current context
|
12
|
+
# SpecForge.context.variables[:user_id] #=> 123
|
13
|
+
#
|
14
|
+
class Context < Data.define(:global, :store, :variables)
|
15
|
+
#
|
16
|
+
# Creates a new context with default values
|
17
|
+
#
|
18
|
+
# @param global [Hash] Global variables shared across all specs
|
19
|
+
# @param variables [Hash] Test variables specific to the current context
|
20
|
+
#
|
21
|
+
# @return [Context] A new context instance
|
22
|
+
#
|
23
|
+
def initialize(global: {}, variables: {})
|
24
|
+
super(
|
25
|
+
global: Global.new(**global),
|
26
|
+
store: Store.new,
|
27
|
+
variables: Variables.new(**variables)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require_relative "context/callbacks"
|
34
|
+
require_relative "context/global"
|
35
|
+
require_relative "context/store"
|
36
|
+
require_relative "context/variables"
|
@@ -2,19 +2,37 @@
|
|
2
2
|
|
3
3
|
return if defined?(SPEC_FORGE_INTERNAL_TESTING)
|
4
4
|
|
5
|
+
#
|
6
|
+
# RSpec's core testing framework module
|
7
|
+
# Provides the fundamental structure and functionality for RSpec tests
|
8
|
+
#
|
5
9
|
module RSpec
|
10
|
+
#
|
11
|
+
# Core implementation details and extensions for RSpec
|
12
|
+
# Contains the fundamental building blocks of the RSpec testing framework
|
13
|
+
#
|
6
14
|
module Core
|
15
|
+
#
|
16
|
+
# Handles notifications and reporting for RSpec test runs
|
17
|
+
# Manages how test results and metadata are processed and communicated
|
18
|
+
#
|
7
19
|
module Notifications
|
8
20
|
#
|
9
|
-
#
|
10
|
-
#
|
21
|
+
# A monkey patch of an internal RSpec class to allow SpecForge to replace parts of
|
22
|
+
# RSpec's reporting output in order to provide useful feedback to the user.
|
23
|
+
# This replaces "rspec" in commands with "spec_forge", removes any line numbers, and
|
24
|
+
# ensures that failures properly report the YAML file that it occurred in.
|
11
25
|
#
|
12
26
|
class SummaryNotification
|
27
|
+
#
|
28
|
+
# Create an alias to RSpec original colorized_rerun_commands so it can be called at a
|
29
|
+
# later point.
|
30
|
+
#
|
31
|
+
alias_method :og_colorized_rerun_commands, :colorized_rerun_commands
|
32
|
+
|
13
33
|
# Customizes RSpec's failure output to:
|
14
34
|
# 1. Use 'spec_forge' instead of 'rspec' for rerun commands
|
15
35
|
# 2. Remove line numbers since SpecForge uses dynamic spec generation
|
16
|
-
alias_method :og_colorized_rerun_commands, :colorized_rerun_commands
|
17
|
-
|
18
36
|
def colorized_rerun_commands(colorizer)
|
19
37
|
# Updating these at this point fixes the re-run for some failures - it depends
|
20
38
|
failed_examples.each do |example|
|
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
|
|