fix 0.19 → 0.21
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/lib/fix/doc.rb +76 -17
- data/lib/fix/error/missing_subject_block.rb +15 -0
- data/lib/fix/set.rb +188 -31
- data/lib/fix.rb +156 -70
- data/lib/kernel.rb +74 -20
- metadata +3 -3
- data/lib/fix/builder.rb +0 -101
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a61d6e74ba8d919c935036f602b4ac3bdf4ec121a9880cb6168d16893384e194
|
4
|
+
data.tar.gz: 1060db9d560d9288af75686c51bf6acbc38fb29606b3d451493f39548795744f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19f6f8612289b9e23bcda11fd20ee039591a8270b1dbec962474261f45e72784ed6e4ab414ecb2e13280095ca349017eb56c3076f4bddf8bc1b5144a44a6880a
|
7
|
+
data.tar.gz: 1a8a4b2181814fbd064559a726cd7a41f6fa1079c9f58fc8babc4d54f539ead621ede997a24754e404e5dc64a37a2e138db07e52df1a9bfb9c6d8c939815abcd
|
data/lib/fix/doc.rb
CHANGED
@@ -3,50 +3,109 @@
|
|
3
3
|
require_relative "error/invalid_specification_name"
|
4
4
|
|
5
5
|
module Fix
|
6
|
-
#
|
6
|
+
# The Doc module serves as a central registry for storing and managing test specifications.
|
7
|
+
# It provides functionality for:
|
8
|
+
# - Storing specification classes in a structured way
|
9
|
+
# - Managing the lifecycle of specification documents
|
10
|
+
# - Extracting test specifications from context objects
|
11
|
+
# - Validating specification names
|
7
12
|
#
|
8
|
-
#
|
9
|
-
#
|
13
|
+
# The module acts as a namespace for specifications, allowing them to be:
|
14
|
+
# - Registered with unique names
|
15
|
+
# - Retrieved by name when needed
|
16
|
+
# - Protected from name collisions
|
17
|
+
# - Organized in a hierarchical structure
|
18
|
+
#
|
19
|
+
# @example Registering a new specification
|
20
|
+
# Fix::Doc.add(:Calculator, calculator_specification_class)
|
21
|
+
#
|
22
|
+
# @example Retrieving specification contexts
|
23
|
+
# contexts = Fix::Doc.fetch(:Calculator)
|
24
|
+
# specifications = Fix::Doc.extract_specifications(*contexts)
|
10
25
|
#
|
11
26
|
# @api private
|
12
27
|
module Doc
|
13
|
-
# Retrieves the contexts
|
28
|
+
# Retrieves the array of test contexts associated with a named specification.
|
29
|
+
# These contexts define the test environment and requirements for the specification.
|
14
30
|
#
|
15
|
-
# @param name [
|
31
|
+
# @param name [Symbol] The name of the specification to retrieve
|
16
32
|
# @return [Array<Fix::Dsl>] Array of context classes for the specification
|
17
|
-
# @raise [NameError] If specification
|
33
|
+
# @raise [NameError] If the specification name doesn't exist in the registry
|
34
|
+
#
|
35
|
+
# @example Retrieving contexts for a calculator specification
|
36
|
+
# contexts = Fix::Doc.fetch(:Calculator)
|
37
|
+
# contexts.each do |context|
|
38
|
+
# # Process each context...
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @api private
|
18
42
|
def self.fetch(name)
|
19
43
|
const_get("#{name}::CONTEXTS")
|
20
44
|
end
|
21
45
|
|
22
|
-
# Extracts test specifications from a list of context classes.
|
23
|
-
#
|
46
|
+
# Extracts complete test specifications from a list of context classes.
|
47
|
+
# This method processes contexts to build a list of executable test specifications.
|
48
|
+
#
|
49
|
+
# Each extracted specification contains:
|
50
|
+
# - The test environment
|
51
|
+
# - The source file location
|
52
|
+
# - The requirement level (MUST, SHOULD, or MAY)
|
53
|
+
# - The list of challenges to execute
|
24
54
|
#
|
25
55
|
# @param contexts [Array<Fix::Dsl>] List of context classes to process
|
26
|
-
# @return [Array<Array>] Array of arrays where each
|
27
|
-
# - [0] environment
|
28
|
-
# - [1] location
|
29
|
-
# - [2] requirement
|
30
|
-
# - [3] challenges
|
56
|
+
# @return [Array<Array>] Array of specification arrays where each contains:
|
57
|
+
# - [0] environment [Fix::Dsl] The test environment instance
|
58
|
+
# - [1] location [String] The test file location ("path:line")
|
59
|
+
# - [2] requirement [Object] The test requirement (MUST, SHOULD, or MAY)
|
60
|
+
# - [3] challenges [Array] Array of test challenges to execute
|
61
|
+
#
|
62
|
+
# @example Extracting specifications from contexts
|
63
|
+
# contexts = Fix::Doc.fetch(:Calculator)
|
64
|
+
# specifications = Fix::Doc.extract_specifications(*contexts)
|
65
|
+
# specifications.each do |env, location, requirement, challenges|
|
66
|
+
# # Process each specification...
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @api private
|
31
70
|
def self.extract_specifications(*contexts)
|
32
71
|
contexts.flat_map do |context|
|
33
72
|
extract_context_specifications(context)
|
34
73
|
end
|
35
74
|
end
|
36
75
|
|
37
|
-
# Registers a new specification class under the given name.
|
76
|
+
# Registers a new specification class under the given name in the registry.
|
77
|
+
# The name must be a valid Ruby constant name to ensure proper namespace organization.
|
38
78
|
#
|
39
|
-
# @param name [
|
79
|
+
# @param name [Symbol] The name to register the specification under
|
40
80
|
# @param klass [Class] The specification class to register
|
41
81
|
# @raise [Fix::Error::InvalidSpecificationName] If name is not a valid constant name
|
42
82
|
# @return [void]
|
43
|
-
|
83
|
+
#
|
84
|
+
# @example Adding a new specification
|
85
|
+
# class CalculatorSpec < Fix::Dsl
|
86
|
+
# # specification implementation...
|
87
|
+
# end
|
88
|
+
# Fix::Doc.add(:Calculator, CalculatorSpec)
|
89
|
+
#
|
90
|
+
# @example Invalid name handling
|
91
|
+
# # This will raise Fix::Error::InvalidSpecificationName
|
92
|
+
# Fix::Doc.add(:"invalid-name", some_class)
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
def self.add(name, klass)
|
44
96
|
const_set(name, klass)
|
45
97
|
rescue ::NameError => _e
|
46
98
|
raise Error::InvalidSpecificationName, name
|
47
99
|
end
|
48
100
|
|
49
|
-
#
|
101
|
+
# Extracts test specifications from a single context class.
|
102
|
+
# This method processes public methods in the context to build
|
103
|
+
# a list of executable test specifications.
|
104
|
+
#
|
105
|
+
# @param context [Fix::Dsl] The context class to process
|
106
|
+
# @return [Array<Array>] Array of specification arrays
|
107
|
+
#
|
108
|
+
# @api private
|
50
109
|
def self.extract_context_specifications(context)
|
51
110
|
env = context.new
|
52
111
|
env.public_methods(false).map do |public_method|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fix
|
4
|
+
module Error
|
5
|
+
# Error raised when attempting to test a specification without providing a subject block
|
6
|
+
class MissingSubjectBlock < ::ArgumentError
|
7
|
+
MISSING_BLOCK_ERROR = "Subject block is required for testing a specification. " \
|
8
|
+
"Use: test { subject } or match? { subject }"
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
super(MISSING_BLOCK_ERROR)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/fix/set.rb
CHANGED
@@ -1,67 +1,224 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "English"
|
4
|
-
|
5
3
|
require_relative "doc"
|
4
|
+
require_relative "dsl"
|
6
5
|
require_relative "run"
|
6
|
+
require_relative "error/missing_subject_block"
|
7
7
|
|
8
8
|
module Fix
|
9
|
-
#
|
9
|
+
# A Set represents a collection of test specifications that can be executed as a test suite.
|
10
|
+
# It manages the lifecycle of specifications, including:
|
11
|
+
# - Building and loading specifications from contexts
|
12
|
+
# - Executing specifications in isolation using process forking
|
13
|
+
# - Reporting test results
|
14
|
+
# - Managing test execution flow and exit status
|
15
|
+
#
|
16
|
+
# @example Creating and running a simple test set
|
17
|
+
# set = Fix::Set.build(:Calculator) do
|
18
|
+
# on(:add, 2, 3) do
|
19
|
+
# it MUST eq 5
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# set.test { Calculator.new }
|
23
|
+
#
|
24
|
+
# @example Loading and running a registered test set
|
25
|
+
# set = Fix::Set.load(:Calculator)
|
26
|
+
# set.match? { Calculator.new } #=> true
|
10
27
|
#
|
11
28
|
# @api private
|
12
29
|
class Set
|
13
|
-
#
|
14
|
-
|
30
|
+
# Builds a new Set from a specification block.
|
31
|
+
#
|
32
|
+
# This method:
|
33
|
+
# 1. Creates a new DSL class for the specification
|
34
|
+
# 2. Evaluates the specification block in this context
|
35
|
+
# 3. Optionally registers the specification under a name
|
36
|
+
# 4. Returns a Set instance ready for testing
|
37
|
+
#
|
38
|
+
# @param name [Symbol, nil] Optional name to register the specification under
|
39
|
+
# @yield Block containing the specification definition using Fix DSL
|
40
|
+
# @return [Fix::Set] A new specification set ready for testing
|
41
|
+
#
|
42
|
+
# @example Building a named specification
|
43
|
+
# Fix::Set.build(:Calculator) do
|
44
|
+
# on(:add, 2, 3) { it MUST eq 5 }
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @example Building an anonymous specification
|
48
|
+
# Fix::Set.build(nil) do
|
49
|
+
# it MUST be_positive
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
def self.build(name, &block)
|
54
|
+
klass = ::Class.new(Dsl)
|
55
|
+
klass.const_set(:CONTEXTS, [klass])
|
56
|
+
klass.instance_eval(&block)
|
57
|
+
Doc.const_set(name, klass) unless name.nil?
|
58
|
+
new(*klass.const_get(:CONTEXTS))
|
59
|
+
end
|
15
60
|
|
16
|
-
#
|
61
|
+
# Loads a previously registered specification set by name.
|
17
62
|
#
|
18
|
-
# @param name [
|
19
|
-
# @return [Set]
|
63
|
+
# @param name [Symbol] The name of the registered specification
|
64
|
+
# @return [Fix::Set] The loaded specification set
|
65
|
+
# @raise [NameError] If the specification name is not found
|
20
66
|
#
|
21
|
-
# @
|
67
|
+
# @example Loading a registered specification
|
68
|
+
# Fix::Set.load(:Calculator) #=> #<Fix::Set:...>
|
69
|
+
#
|
70
|
+
# @api private
|
22
71
|
def self.load(name)
|
23
72
|
new(*Doc.fetch(name))
|
24
73
|
end
|
25
74
|
|
26
|
-
#
|
75
|
+
# Initializes a new Set with given test contexts.
|
27
76
|
#
|
28
|
-
#
|
77
|
+
# The contexts are processed to extract test specifications and
|
78
|
+
# randomized to ensure test isolation and catch order dependencies.
|
79
|
+
#
|
80
|
+
# @param contexts [Array<Fix::Dsl>] List of specification contexts to include
|
81
|
+
#
|
82
|
+
# @example Creating a set with multiple contexts
|
83
|
+
# Fix::Set.new(base_context, admin_context, guest_context)
|
29
84
|
def initialize(*contexts)
|
30
|
-
@
|
85
|
+
@expected = Doc.extract_specifications(*contexts).shuffle
|
31
86
|
end
|
32
87
|
|
33
|
-
#
|
88
|
+
# Verifies if a subject matches all specifications without exiting.
|
89
|
+
#
|
90
|
+
# This method is useful for:
|
91
|
+
# - Conditional testing where exit on failure is not desired
|
92
|
+
# - Integration into larger test suites
|
93
|
+
# - Programmatic test result handling
|
94
|
+
#
|
95
|
+
# @yield Block that returns the subject to test
|
96
|
+
# @yieldreturn [Object] The subject to test against specifications
|
97
|
+
# @return [Boolean] true if all tests pass, false otherwise
|
98
|
+
# @raise [Error::MissingSubjectBlock] If no subject block is provided
|
99
|
+
#
|
100
|
+
# @example Basic matching
|
101
|
+
# set.match? { Calculator.new } #=> true
|
34
102
|
#
|
35
|
-
# @
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
103
|
+
# @example Conditional testing
|
104
|
+
# if set.match? { user_input }
|
105
|
+
# process_valid_input(user_input)
|
106
|
+
# else
|
107
|
+
# handle_invalid_input
|
108
|
+
# end
|
39
109
|
#
|
40
110
|
# @api public
|
41
|
-
def
|
42
|
-
|
111
|
+
def match?(&subject)
|
112
|
+
raise Error::MissingSubjectBlock unless subject
|
113
|
+
|
114
|
+
expected.all? { |spec| run_spec(*spec, &subject) }
|
43
115
|
end
|
44
116
|
|
45
|
-
|
117
|
+
# Executes the complete test suite against a subject.
|
118
|
+
#
|
119
|
+
# This method provides a comprehensive test run that:
|
120
|
+
# - Executes all specifications in random order
|
121
|
+
# - Runs each test in isolation via process forking
|
122
|
+
# - Reports results for each specification
|
123
|
+
# - Exits with appropriate status code
|
124
|
+
#
|
125
|
+
# @yield Block that returns the subject to test
|
126
|
+
# @yieldreturn [Object] The subject to test against specifications
|
127
|
+
# @return [Boolean] true if all tests pass
|
128
|
+
# @raise [SystemExit] Exits with status 1 if any test fails
|
129
|
+
# @raise [Error::MissingSubjectBlock] If no subject block is provided
|
130
|
+
#
|
131
|
+
# @example Basic test execution
|
132
|
+
# set.test { Calculator.new }
|
133
|
+
#
|
134
|
+
# @example Testing with dependencies
|
135
|
+
# set.test {
|
136
|
+
# calc = Calculator.new
|
137
|
+
# calc.precision = :high
|
138
|
+
# calc
|
139
|
+
# }
|
140
|
+
#
|
141
|
+
# @api public
|
142
|
+
def test(&subject)
|
143
|
+
match?(&subject) || exit_with_failure
|
144
|
+
end
|
46
145
|
|
47
|
-
|
48
|
-
|
146
|
+
# Returns a string representation of the test set.
|
147
|
+
#
|
148
|
+
# @return [String] Human-readable description of the test set
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# set.to_s #=> "fix [<specification list>]"
|
152
|
+
#
|
153
|
+
# @api public
|
154
|
+
def to_s
|
155
|
+
"fix #{expected.inspect}"
|
49
156
|
end
|
50
157
|
|
158
|
+
private
|
159
|
+
|
160
|
+
# List of specifications to be tested.
|
161
|
+
# Each specification is an array containing:
|
162
|
+
# - [0] environment: Fix::Dsl instance for test context
|
163
|
+
# - [1] location: String indicating source file and line
|
164
|
+
# - [2] requirement: Test requirement (MUST, SHOULD, or MAY)
|
165
|
+
# - [3] challenges: Array of test challenges to execute
|
166
|
+
#
|
167
|
+
# @return [Array<Array>] List of specification arrays
|
168
|
+
attr_reader :expected
|
169
|
+
|
170
|
+
# Executes a single specification in an isolated process.
|
171
|
+
#
|
172
|
+
# @param env [Fix::Dsl] Test environment instance
|
173
|
+
# @param location [String] Source location (file:line)
|
174
|
+
# @param requirement [Object] Test requirement
|
175
|
+
# @param challenges [Array] Test challenges
|
176
|
+
# @yield Block returning the subject to test
|
177
|
+
# @return [Boolean] true if specification passed
|
51
178
|
def run_spec(env, location, requirement, challenges, &subject)
|
52
|
-
::Process.fork {
|
53
|
-
::Process.
|
54
|
-
|
179
|
+
child_pid = ::Process.fork { execute_spec(env, location, requirement, challenges, &subject) }
|
180
|
+
_pid, process_status = ::Process.wait2(child_pid)
|
181
|
+
process_status.success?
|
182
|
+
end
|
183
|
+
|
184
|
+
# Executes a specification in the current process.
|
185
|
+
#
|
186
|
+
# @param env [Fix::Dsl] Test environment instance
|
187
|
+
# @param location [String] Source location (file:line)
|
188
|
+
# @param requirement [Object] Test requirement
|
189
|
+
# @param challenges [Array] Test challenges
|
190
|
+
# @yield Block returning the subject to test
|
191
|
+
# @return [void]
|
192
|
+
def execute_spec(env, location, requirement, challenges, &subject)
|
193
|
+
result = Run.new(env, requirement, *challenges).test(&subject)
|
194
|
+
report_result(location, result)
|
195
|
+
exit_with_status(result.passed?)
|
55
196
|
end
|
56
197
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
198
|
+
# Reports the result of a specification execution.
|
199
|
+
#
|
200
|
+
# @param location [String] Source location (file:line)
|
201
|
+
# @param result [Object] Test execution result
|
202
|
+
# @return [void]
|
203
|
+
def report_result(location, result)
|
204
|
+
puts "#{location} #{result.colored_string}"
|
61
205
|
end
|
62
206
|
|
63
|
-
|
64
|
-
|
207
|
+
# Exits the process with a failure status.
|
208
|
+
#
|
209
|
+
# @return [void]
|
210
|
+
# @raise [SystemExit] Always exits with status 1
|
211
|
+
def exit_with_failure
|
212
|
+
::Kernel.exit(false)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Exits the process with the given status.
|
216
|
+
#
|
217
|
+
# @param status [Boolean] Exit status to use
|
218
|
+
# @return [void]
|
219
|
+
# @raise [SystemExit] Always exits with provided status
|
220
|
+
def exit_with_status(status)
|
221
|
+
::Kernel.exit(status)
|
65
222
|
end
|
66
223
|
end
|
67
224
|
end
|
data/lib/fix.rb
CHANGED
@@ -1,93 +1,179 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "fix/doc"
|
4
|
+
require_relative "fix/error/missing_specification_block"
|
4
5
|
require_relative "fix/error/specification_not_found"
|
5
6
|
require_relative "fix/set"
|
6
7
|
require_relative "kernel"
|
7
8
|
|
8
|
-
#
|
9
|
+
# The Fix framework namespace provides core functionality for managing and running test specifications.
|
10
|
+
# Fix offers a unique approach to testing by clearly separating specifications from their implementations.
|
9
11
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# 2. Anonymous specifications for immediate testing
|
12
|
+
# Fix supports two primary modes of operation:
|
13
|
+
# 1. Named specifications that can be stored and referenced later
|
14
|
+
# 2. Anonymous specifications for immediate one-time testing
|
14
15
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
16
|
+
# Available matchers through the Matchi library include:
|
17
|
+
# - Basic Comparison: eq, eql, be, equal
|
18
|
+
# - Type Checking: be_an_instance_of, be_a_kind_of
|
19
|
+
# - State & Changes: change(object, method).by(n), by_at_least(n), by_at_most(n), from(old).to(new), to(new)
|
20
|
+
# - Value Testing: be_within(delta).of(value), match(regex), satisfy { |value| ... }
|
21
|
+
# - Exceptions: raise_exception(class)
|
22
|
+
# - State Testing: be_true, be_false, be_nil
|
23
|
+
# - Predicate Matchers: be_*, have_* (e.g., be_empty, have_key)
|
24
|
+
#
|
25
|
+
# @example Creating and running a named specification with various matchers
|
26
|
+
# Fix :Calculator do
|
27
|
+
# on(:add, 0.1, 0.2) do
|
28
|
+
# it SHOULD be 0.3 # Technically true but fails due to floating point precision
|
29
|
+
# it MUST be_an_instance_of(Float) # Type checking
|
30
|
+
# it MUST be_within(0.0001).of(0.3) # Proper floating point comparison
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# on(:divide, 1, 0) do
|
34
|
+
# it MUST raise_exception ZeroDivisionError # Exception testing
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# Fix[:Calculator].test { Calculator.new }
|
39
|
+
#
|
40
|
+
# @example Using state change matchers
|
41
|
+
# Fix :UserAccount do
|
42
|
+
# on(:deposit, 100) do
|
43
|
+
# it MUST change(account, :balance).by(100)
|
44
|
+
# it SHOULD change(account, :updated_at)
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# on(:update_status, :premium) do
|
48
|
+
# it MUST change(account, :status).from(:basic).to(:premium)
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @example Using predicate matchers
|
53
|
+
# Fix :Collection do
|
54
|
+
# with items: [] do
|
55
|
+
# it MUST be_empty # Tests empty?
|
56
|
+
# it MUST_NOT have_errors # Tests has_errors?
|
57
|
+
# end
|
18
58
|
# end
|
19
59
|
#
|
20
|
-
#
|
60
|
+
# @example Complete specification with multiple matchers
|
61
|
+
# Fix :Product do
|
62
|
+
# let(:price) { 42.99 }
|
21
63
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# it MUST be_positive
|
25
|
-
# end.test { 42 }
|
64
|
+
# it MUST be_an_instance_of Product # Type checking
|
65
|
+
# it MUST_NOT be_nil # Nil checking
|
26
66
|
#
|
27
|
-
#
|
28
|
-
#
|
67
|
+
# on(:price) do
|
68
|
+
# it MUST be_within(0.01).of(42.99) # Floating point comparison
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# with category: "electronics" do
|
72
|
+
# it MUST satisfy { |p| p.valid? } # Custom validation
|
73
|
+
#
|
74
|
+
# on(:save) do
|
75
|
+
# it MUST change(product, :updated_at) # State change
|
76
|
+
# it SHOULD_NOT raise_exception # Exception checking
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# @see Fix::Set For managing collections of specifications
|
82
|
+
# @see Fix::Doc For storing and retrieving specifications
|
83
|
+
# @see Fix::Dsl For the domain-specific language used in specifications
|
84
|
+
# @see Fix::Matcher For the complete list of available matchers
|
29
85
|
#
|
30
86
|
# @api public
|
31
87
|
module Fix
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
88
|
+
# Creates a new specification set, optionally registering it under a name.
|
89
|
+
#
|
90
|
+
# @param name [Symbol, nil] Optional name to register the specification under.
|
91
|
+
# If nil, creates an anonymous specification for immediate use.
|
92
|
+
# @yieldreturn [void] Block containing the specification definition using Fix DSL
|
93
|
+
# @return [Fix::Set] A new specification set ready for testing
|
94
|
+
# @raise [Fix::Error::MissingSpecificationBlock] If no block is provided
|
95
|
+
#
|
96
|
+
# @example Create a named specification
|
97
|
+
# Fix :StringValidator do
|
98
|
+
# on(:validate, "hello@example.com") do
|
99
|
+
# it MUST be_valid_email
|
100
|
+
# it MUST satisfy { |result| result.errors.empty? }
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# @example Create an anonymous specification
|
105
|
+
# Fix do
|
106
|
+
# it MUST be_positive
|
107
|
+
# it MUST be_within(0.1).of(42.0)
|
108
|
+
# end.test { 42 }
|
109
|
+
#
|
110
|
+
# @api public
|
111
|
+
def self.spec(name = nil, &block)
|
112
|
+
raise Error::MissingSpecificationBlock if block.nil?
|
46
113
|
|
47
|
-
|
48
|
-
|
49
|
-
# @example Get all specification names
|
50
|
-
# Fix.specification_names #=> [:Answer, :Calculator, :UserProfile]
|
51
|
-
#
|
52
|
-
# @return [Array<Symbol>] Sorted array of specification names
|
53
|
-
def specification_names
|
54
|
-
Doc.constants.sort
|
55
|
-
end
|
56
|
-
|
57
|
-
# Checks if a specification is defined.
|
58
|
-
#
|
59
|
-
# @example Check for specification existence
|
60
|
-
# Fix.spec_defined?(:Answer) #=> true
|
61
|
-
#
|
62
|
-
# @param name [String, Symbol] Name of the specification to check
|
63
|
-
# @return [Boolean] true if specification exists, false otherwise
|
64
|
-
def spec_defined?(name)
|
65
|
-
specification_names.include?(normalize_name(name))
|
66
|
-
end
|
114
|
+
Set.build(name, &block)
|
115
|
+
end
|
67
116
|
|
68
|
-
|
117
|
+
# Retrieves a previously registered specification by name.
|
118
|
+
#
|
119
|
+
# @param name [Symbol] The constant name of the specification to retrieve
|
120
|
+
# @return [Fix::Set] The loaded specification set ready for testing
|
121
|
+
# @raise [Fix::Error::SpecificationNotFound] If the named specification doesn't exist
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# # Define a specification with multiple matchers
|
125
|
+
# Fix :EmailValidator do
|
126
|
+
# on(:validate, "test@example.com") do
|
127
|
+
# it MUST be_valid
|
128
|
+
# it MUST_NOT raise_exception
|
129
|
+
# it SHOULD satisfy { |result| result.score > 0.8 }
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# # Later, retrieve and use it
|
134
|
+
# Fix[:EmailValidator].test { MyEmailValidator }
|
135
|
+
#
|
136
|
+
# @see #spec For creating new specifications
|
137
|
+
# @see Fix::Set#test For running tests against a specification
|
138
|
+
# @see Fix::Matcher For available matchers
|
139
|
+
#
|
140
|
+
# @api public
|
141
|
+
def self.[](name)
|
142
|
+
raise Error::SpecificationNotFound, name unless key?(name)
|
69
143
|
|
70
|
-
|
71
|
-
|
72
|
-
#
|
73
|
-
# @param name [String, Symbol] The name to normalize
|
74
|
-
# @return [Symbol] The normalized name
|
75
|
-
# @example
|
76
|
-
# normalize_name("Answer") #=> :Answer
|
77
|
-
# normalize_name(:Answer) #=> :Answer
|
78
|
-
def normalize_name(name)
|
79
|
-
String(name).to_sym
|
80
|
-
end
|
144
|
+
Set.load(name)
|
145
|
+
end
|
81
146
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
147
|
+
# Lists all defined specification names.
|
148
|
+
#
|
149
|
+
# @return [Array<Symbol>] Sorted array of registered specification names
|
150
|
+
#
|
151
|
+
# @example
|
152
|
+
# Fix :First do; end
|
153
|
+
# Fix :Second do; end
|
154
|
+
#
|
155
|
+
# Fix.keys #=> [:First, :Second]
|
156
|
+
#
|
157
|
+
# @api public
|
158
|
+
def self.keys
|
159
|
+
Doc.constants.sort
|
160
|
+
end
|
89
161
|
|
90
|
-
|
91
|
-
|
162
|
+
# Checks if a specification is registered under the given name.
|
163
|
+
#
|
164
|
+
# @param name [Symbol] The name to check for
|
165
|
+
# @return [Boolean] true if a specification exists with this name, false otherwise
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# Fix :Example do
|
169
|
+
# it MUST be_an_instance_of(Example)
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# Fix.key?(:Example) #=> true
|
173
|
+
# Fix.key?(:Missing) #=> false
|
174
|
+
#
|
175
|
+
# @api public
|
176
|
+
def self.key?(name)
|
177
|
+
keys.include?(name)
|
92
178
|
end
|
93
179
|
end
|
data/lib/kernel.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "fix/builder"
|
4
|
-
|
5
3
|
# Extension of the global Kernel module to provide the Fix method.
|
6
|
-
# This
|
7
|
-
#
|
4
|
+
# This module provides a global entry point to the Fix testing framework,
|
5
|
+
# allowing specifications to be defined and managed from anywhere in the application.
|
6
|
+
#
|
7
|
+
# The Fix method can be used in two main ways:
|
8
|
+
# 1. Creating named specifications for reuse
|
9
|
+
# 2. Creating anonymous specifications for immediate testing
|
8
10
|
#
|
9
11
|
# @api public
|
10
12
|
module Kernel
|
@@ -14,35 +16,87 @@ module Kernel
|
|
14
16
|
# both a namespace and a method name, following Ruby conventions for DSLs.
|
15
17
|
|
16
18
|
# Defines a new test specification or creates an anonymous specification set.
|
17
|
-
# When a name is provided, the specification is registered globally and can
|
18
|
-
#
|
19
|
+
# When a name is provided, the specification is registered globally and can be
|
20
|
+
# referenced later using Fix[name]. Anonymous specifications are executed
|
19
21
|
# immediately and cannot be referenced later.
|
20
22
|
#
|
21
|
-
#
|
23
|
+
# Specifications can use three levels of requirements, following RFC 2119:
|
24
|
+
# - MUST/MUST_NOT: Absolute requirements or prohibitions
|
25
|
+
# - SHOULD/SHOULD_NOT: Strong recommendations that can be ignored with good reason
|
26
|
+
# - MAY: Optional features that can be implemented or not
|
27
|
+
#
|
28
|
+
# Available matchers include:
|
29
|
+
# - Basic Comparison: eq(expected), eql(expected), be(expected), equal(expected)
|
30
|
+
# - Type Checking: be_an_instance_of(class), be_a_kind_of(class)
|
31
|
+
# - State & Changes: change(object, method).by(n), by_at_least(n), by_at_most(n),
|
32
|
+
# from(old).to(new), to(new)
|
33
|
+
# - Value Testing: be_within(delta).of(value), match(regex),
|
34
|
+
# satisfy { |value| ... }
|
35
|
+
# - Exceptions: raise_exception(class)
|
36
|
+
# - State Testing: be_true, be_false, be_nil
|
37
|
+
# - Predicate Matchers: be_* (e.g., be_empty), have_* (e.g., have_key)
|
38
|
+
#
|
39
|
+
# @example Creating a named specification with multiple contexts and matchers
|
22
40
|
# Fix :Calculator do
|
23
41
|
# on(:add, 2, 3) do
|
24
|
-
# it MUST
|
42
|
+
# it MUST eq 5
|
43
|
+
# it MUST be_an_instance_of(Integer)
|
25
44
|
# end
|
26
|
-
# end
|
27
45
|
#
|
28
|
-
#
|
29
|
-
#
|
46
|
+
# with precision: :high do
|
47
|
+
# on(:divide, 10, 3) do
|
48
|
+
# it MUST be_within(0.001).of(3.333)
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# on(:divide, 1, 0) do
|
53
|
+
# it MUST raise_exception ZeroDivisionError
|
54
|
+
# end
|
55
|
+
# end
|
30
56
|
#
|
31
57
|
# @example Creating and immediately testing an anonymous specification
|
32
58
|
# Fix do
|
33
59
|
# it MUST be_positive
|
60
|
+
# it SHOULD be_even
|
61
|
+
# it MAY be_prime
|
34
62
|
# end.test { 42 }
|
35
63
|
#
|
36
|
-
# @
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
64
|
+
# @example Testing state changes
|
65
|
+
# Fix :Account do
|
66
|
+
# on(:deposit, 100) do
|
67
|
+
# it MUST change(account, :balance).by(100)
|
68
|
+
# it SHOULD change(account, :updated_at)
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# on(:withdraw, 50) do
|
72
|
+
# it MUST change(account, :balance).by(-50)
|
73
|
+
# it MUST_NOT change(account, :status)
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# @example Using predicates and custom matchers
|
78
|
+
# Fix :Collection do
|
79
|
+
# with items: [] do
|
80
|
+
# it MUST be_empty
|
81
|
+
# it MUST_NOT have_errors
|
82
|
+
# it SHOULD satisfy { |c| c.valid? && c.initialized? }
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# @param name [Symbol, nil] Optional name to register the specification under
|
87
|
+
# @yield Block containing the specification definition using Fix DSL
|
88
|
+
# @return [Fix::Set] A specification set ready for testing
|
89
|
+
# @raise [Fix::Error::MissingSpecificationBlock] If no block is provided
|
90
|
+
# @raise [Fix::Error::InvalidSpecificationName] If name is not a valid constant name
|
91
|
+
#
|
92
|
+
# @see Fix::Set For managing collections of specifications
|
93
|
+
# @see Fix::Dsl For the domain-specific language used in specifications
|
94
|
+
# @see Fix::Matcher For the complete list of available matchers
|
95
|
+
# @see https://tools.ietf.org/html/rfc2119 For details about requirement levels
|
40
96
|
#
|
41
|
-
# @
|
42
|
-
|
43
|
-
|
44
|
-
def Fix(name = nil, &)
|
45
|
-
::Fix::Builder.build(name, &)
|
97
|
+
# @api public
|
98
|
+
def Fix(name = nil, &block)
|
99
|
+
::Fix.spec(name, &block)
|
46
100
|
end
|
47
101
|
|
48
102
|
# rubocop:enable Naming/MethodName
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fix
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.21'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: defi
|
@@ -65,11 +65,11 @@ files:
|
|
65
65
|
- LICENSE.md
|
66
66
|
- README.md
|
67
67
|
- lib/fix.rb
|
68
|
-
- lib/fix/builder.rb
|
69
68
|
- lib/fix/doc.rb
|
70
69
|
- lib/fix/dsl.rb
|
71
70
|
- lib/fix/error/invalid_specification_name.rb
|
72
71
|
- lib/fix/error/missing_specification_block.rb
|
72
|
+
- lib/fix/error/missing_subject_block.rb
|
73
73
|
- lib/fix/error/specification_not_found.rb
|
74
74
|
- lib/fix/matcher.rb
|
75
75
|
- lib/fix/requirement.rb
|
data/lib/fix/builder.rb
DELETED
@@ -1,101 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "doc"
|
4
|
-
require_relative "dsl"
|
5
|
-
require_relative "set"
|
6
|
-
require_relative "error/missing_specification_block"
|
7
|
-
|
8
|
-
module Fix
|
9
|
-
# Handles the creation and setup of Fix specifications.
|
10
|
-
#
|
11
|
-
# The Builder constructs new Fix specification sets following these steps:
|
12
|
-
# 1. Creates a new specification class inheriting from DSL
|
13
|
-
# 2. Defines the specification content using the provided block
|
14
|
-
# 3. Optionally registers the named specification
|
15
|
-
# 4. Returns the built specification set
|
16
|
-
#
|
17
|
-
# @example Create a named specification
|
18
|
-
# Fix::Builder.build(:Calculator) do
|
19
|
-
# on(:add, 2, 3) { it MUST equal 5 }
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# @example Create an anonymous specification
|
23
|
-
# Fix::Builder.build do
|
24
|
-
# it MUST be_positive
|
25
|
-
# end
|
26
|
-
#
|
27
|
-
# @see Fix::Set
|
28
|
-
# @see Fix::Dsl
|
29
|
-
# @api private
|
30
|
-
class Builder
|
31
|
-
# Creates a new specification set.
|
32
|
-
#
|
33
|
-
# @param name [String, Symbol, nil] Optional name for the specification
|
34
|
-
# @yieldparam [void] Block containing specification definitions
|
35
|
-
# @yieldreturn [void]
|
36
|
-
# @return [Fix::Set] The constructed specification set
|
37
|
-
# @raise [Fix::Error::InvalidSpecificationName] If name is invalid
|
38
|
-
# @raise [Fix::Error::MissingSpecificationBlock] If no block given
|
39
|
-
def self.build(name = nil, &block)
|
40
|
-
new(name, &block).construct_set
|
41
|
-
end
|
42
|
-
|
43
|
-
# @return [String, Symbol, nil] The name of the specification
|
44
|
-
attr_reader :name
|
45
|
-
|
46
|
-
def initialize(name = nil, &block)
|
47
|
-
raise Error::MissingSpecificationBlock unless block
|
48
|
-
|
49
|
-
@name = name
|
50
|
-
@block = block
|
51
|
-
end
|
52
|
-
|
53
|
-
# Constructs and returns a new specification set.
|
54
|
-
#
|
55
|
-
# @return [Fix::Set] The constructed specification set
|
56
|
-
def construct_set
|
57
|
-
klass = create_specification
|
58
|
-
populate_specification(klass)
|
59
|
-
register_if_named(klass)
|
60
|
-
build_set(klass)
|
61
|
-
end
|
62
|
-
|
63
|
-
private
|
64
|
-
|
65
|
-
# @return [Proc] The block containing specification definitions
|
66
|
-
attr_reader :block
|
67
|
-
|
68
|
-
# Creates a new specification class with context tracking.
|
69
|
-
#
|
70
|
-
# @return [Class] A new class inheriting from Fix::Dsl with CONTEXTS initialized
|
71
|
-
def create_specification
|
72
|
-
::Class.new(Dsl).tap do |klass|
|
73
|
-
klass.const_set(:CONTEXTS, [klass])
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Evaluates the specification block in the context of the class.
|
78
|
-
#
|
79
|
-
# @param klass [Class] The class to populate with specifications
|
80
|
-
# @return [void]
|
81
|
-
def populate_specification(klass)
|
82
|
-
klass.instance_eval(&block)
|
83
|
-
end
|
84
|
-
|
85
|
-
# Registers the specification in Fix::Doc if a name was provided.
|
86
|
-
#
|
87
|
-
# @param klass [Class] The specification class to register
|
88
|
-
# @return [void]
|
89
|
-
def register_if_named(klass)
|
90
|
-
Doc.spec_set(name, klass) if name
|
91
|
-
end
|
92
|
-
|
93
|
-
# Creates a new specification set from the populated class.
|
94
|
-
#
|
95
|
-
# @param klass [Class] The populated specification class
|
96
|
-
# @return [Fix::Set] A new specification set
|
97
|
-
def build_set(klass)
|
98
|
-
Set.new(*klass.const_get(:CONTEXTS))
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|