fix 0.20 → 0.21

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fix/doc.rb +76 -17
  3. data/lib/fix/set.rb +127 -113
  4. data/lib/fix.rb +156 -70
  5. data/lib/kernel.rb +74 -20
  6. metadata +2 -3
  7. data/lib/fix/builder.rb +0 -101
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb6b67aefa0647614856cce193a81deb17a4310561e48c3d4184b0b1856b0bdc
4
- data.tar.gz: a66456067810448477560424cd6f51b23ab9768013f344b598da3a61cc856d2b
3
+ metadata.gz: a61d6e74ba8d919c935036f602b4ac3bdf4ec121a9880cb6168d16893384e194
4
+ data.tar.gz: 1060db9d560d9288af75686c51bf6acbc38fb29606b3d451493f39548795744f
5
5
  SHA512:
6
- metadata.gz: 7cf39f73a43eaee6b19de405903c8421707f35fd7c3ccf06ff0395c434a3e9f701c596f5ba1ced62fa7edaef80fe17cfbfd6d275ab2716330c3feccfa6a9695c
7
- data.tar.gz: 0b23915ab51f8c6d34a013188a07130c6d86eab5e128ddaeae4ef53b3952257ff5e49ef729fae5aaa49064cdb2647d4cd37ddd1fbaed2189170a1ac8834d65c4
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
- # Module for storing and managing specification documents.
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
- # This module acts as a registry for specification classes and handles
9
- # the extraction of test specifications from context objects.
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 array for a named specification.
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 [String, Symbol] The constant name of the specification
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 constant is not found
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
- # Each specification consists of an environment and its associated test data.
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 sub-array contains:
27
- # - [0] environment: The test environment instance
28
- # - [1] location: The test file location (as "path:line")
29
- # - [2] requirement: The test requirement (MUST, SHOULD, or MAY)
30
- # - [3] challenges: Array of test challenges to execute
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 [String, Symbol] Name to register the specification under
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
- def self.spec_set(name, klass)
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
- # @private
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|
data/lib/fix/set.rb CHANGED
@@ -1,108 +1,110 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "doc"
4
+ require_relative "dsl"
4
5
  require_relative "run"
5
6
  require_relative "error/missing_subject_block"
6
7
 
7
8
  module Fix
8
- # Collection of specifications that can be executed as a test suite.
9
- #
10
- # The Set class is a central component in Fix's architecture that handles:
11
- # - Loading and organizing test specifications
12
- # - Managing test execution and isolation
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
13
  # - Reporting test results
14
- # - Handling process management for test isolation
15
- #
16
- # It supports both named specifications (loaded via Fix[name]) and anonymous
17
- # specifications (created directly via Fix blocks).
14
+ # - Managing test execution flow and exit status
18
15
  #
19
- # @example Running a simple named specification
20
- # Fix[:Calculator].test { Calculator.new }
21
- #
22
- # @example Running a complex specification with multiple contexts
23
- # Fix[:UserSystem] do
24
- # with(role: "admin") do
25
- # on :access?, :settings do
26
- # it MUST be_true
27
- # end
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
28
20
  # end
29
- #
30
- # with(role: "guest") do
31
- # on :access?, :settings do
32
- # it MUST be_false
33
- # end
34
- # end
35
- # end.test { UserSystem.new(role:) }
36
- #
37
- # @example Using match? for conditional testing
38
- # if Fix[:EmailValidator].match? { email }
39
- # puts "Email is valid"
40
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
41
27
  #
42
28
  # @api private
43
29
  class Set
44
- # List of specifications to be tested.
45
- # Each specification is an array containing:
46
- # - The test environment
47
- # - The source location (file:line)
48
- # - The requirement (MUST, SHOULD, or MAY)
49
- # - The challenges to apply
30
+ # Builds a new Set from a specification block.
50
31
  #
51
- # @return [Array] List of specifications
52
- attr_reader :expected
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
53
60
 
54
- class << self
55
- # Loads specifications from a registered constant name.
56
- #
57
- # This method retrieves previously registered specifications and creates
58
- # a new Set instance ready for testing. It's typically used in conjunction
59
- # with Fix[name] syntax.
60
- #
61
- # @param name [String, Symbol] The constant name of the specifications
62
- # @return [Set] A new Set instance containing the loaded specifications
63
- # @raise [Fix::Error::SpecificationNotFound] If specification doesn't exist
64
- #
65
- # @example Loading a named specification
66
- # Fix::Set.load(:Calculator)
67
- #
68
- # @example Loading and testing in one go
69
- # Fix::Set.load(:EmailValidator).test { email }
70
- #
71
- # @api public
72
- def load(name)
73
- new(*Doc.fetch(name))
74
- end
61
+ # Loads a previously registered specification set by name.
62
+ #
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
66
+ #
67
+ # @example Loading a registered specification
68
+ # Fix::Set.load(:Calculator) #=> #<Fix::Set:...>
69
+ #
70
+ # @api private
71
+ def self.load(name)
72
+ new(*Doc.fetch(name))
75
73
  end
76
74
 
77
- # Initialize a new Set with the given contexts.
75
+ # Initializes a new Set with given test contexts.
78
76
  #
79
- # @param contexts [Array<Fix::Dsl>] List of specification contexts
77
+ # The contexts are processed to extract test specifications and
78
+ # randomized to ensure test isolation and catch order dependencies.
80
79
  #
81
- # @example Creating a set with a single context
82
- # Fix::Set.new(calculator_context)
80
+ # @param contexts [Array<Fix::Dsl>] List of specification contexts to include
83
81
  #
84
82
  # @example Creating a set with multiple contexts
85
83
  # Fix::Set.new(base_context, admin_context, guest_context)
86
84
  def initialize(*contexts)
87
- @expected = randomize_specs(Doc.extract_specifications(*contexts))
85
+ @expected = Doc.extract_specifications(*contexts).shuffle
88
86
  end
89
87
 
90
- # Checks if the subject matches all specifications without exiting.
88
+ # Verifies if a subject matches all specifications without exiting.
91
89
  #
92
- # Unlike #test, this method:
93
- # - Returns a boolean instead of exiting
94
- # - Can be used in conditional logic
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
95
94
  #
96
- # @yield The block of code to be tested
97
- # @yieldreturn [Object] The result of the code being tested
95
+ # @yield Block that returns the subject to test
96
+ # @yieldreturn [Object] The subject to test against specifications
98
97
  # @return [Boolean] true if all tests pass, false otherwise
98
+ # @raise [Error::MissingSubjectBlock] If no subject block is provided
99
99
  #
100
- # @example Basic usage
101
- # set.match? { Calculator.new } #=> true
100
+ # @example Basic matching
101
+ # set.match? { Calculator.new } #=> true
102
102
  #
103
- # @example Conditional usage
103
+ # @example Conditional testing
104
104
  # if set.match? { user_input }
105
- # save_to_database(user_input)
105
+ # process_valid_input(user_input)
106
+ # else
107
+ # handle_invalid_input
106
108
  # end
107
109
  #
108
110
  # @api public
@@ -112,33 +114,41 @@ module Fix
112
114
  expected.all? { |spec| run_spec(*spec, &subject) }
113
115
  end
114
116
 
115
- # Runs the test suite against the provided subject.
117
+ # Executes the complete test suite against a subject.
116
118
  #
117
- # This method:
119
+ # This method provides a comprehensive test run that:
118
120
  # - Executes all specifications in random order
119
- # - Runs each test in isolation using process forking
121
+ # - Runs each test in isolation via process forking
120
122
  # - Reports results for each specification
121
- # - Exits with failure if any test fails
123
+ # - Exits with appropriate status code
122
124
  #
123
- # @yield The block of code to be tested
124
- # @yieldreturn [Object] The result of the code being tested
125
+ # @yield Block that returns the subject to test
126
+ # @yieldreturn [Object] The subject to test against specifications
125
127
  # @return [Boolean] true if all tests pass
126
- # @raise [SystemExit] When any test fails (exit code: 1)
128
+ # @raise [SystemExit] Exits with status 1 if any test fails
129
+ # @raise [Error::MissingSubjectBlock] If no subject block is provided
127
130
  #
128
- # @example Basic usage
131
+ # @example Basic test execution
129
132
  # set.test { Calculator.new }
130
133
  #
131
- # @example Testing with parameters
132
- # set.test { Game.new(south_variant:, north_variant:) }
134
+ # @example Testing with dependencies
135
+ # set.test {
136
+ # calc = Calculator.new
137
+ # calc.precision = :high
138
+ # calc
139
+ # }
133
140
  #
134
141
  # @api public
135
142
  def test(&subject)
136
143
  match?(&subject) || exit_with_failure
137
144
  end
138
145
 
139
- # Returns a string representing the matcher.
146
+ # Returns a string representation of the test set.
147
+ #
148
+ # @return [String] Human-readable description of the test set
140
149
  #
141
- # @return [String] a human-readable description of the matcher
150
+ # @example
151
+ # set.to_s #=> "fix [<specification list>]"
142
152
  #
143
153
  # @api public
144
154
  def to_s
@@ -147,62 +157,66 @@ module Fix
147
157
 
148
158
  private
149
159
 
150
- # Randomizes the order of specifications for better isolation
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
151
166
  #
152
- # @param specifications [Array] The specifications to randomize
153
- # @return [Array] Randomized specifications
154
- def randomize_specs(specifications)
155
- specifications.shuffle
156
- end
167
+ # @return [Array<Array>] List of specification arrays
168
+ attr_reader :expected
157
169
 
158
- # Runs a single specification in a forked process
159
- #
160
- # @param env [Fix::Dsl] The test environment
161
- # @param location [String] The source location of the spec
162
- # @param requirement [Object] The test requirement
163
- # @param challenges [Array] The test challenges
164
- # @yield The subject block to test against
165
- # @return [Boolean] true if spec passed
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
166
178
  def run_spec(env, location, requirement, challenges, &subject)
167
179
  child_pid = ::Process.fork { execute_spec(env, location, requirement, challenges, &subject) }
168
180
  _pid, process_status = ::Process.wait2(child_pid)
169
181
  process_status.success?
170
182
  end
171
183
 
172
- # Executes a specification in its own process
184
+ # Executes a specification in the current process.
173
185
  #
174
- # @param env [Fix::Dsl] The test environment
175
- # @param location [String] The source location of the spec
176
- # @param requirement [Object] The test requirement
177
- # @param challenges [Array] The test challenges
178
- # @yield The subject block to test against
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]
179
192
  def execute_spec(env, location, requirement, challenges, &subject)
180
193
  result = Run.new(env, requirement, *challenges).test(&subject)
181
194
  report_result(location, result)
182
195
  exit_with_status(result.passed?)
183
196
  end
184
197
 
185
- # Reports the result of a specification
198
+ # Reports the result of a specification execution.
186
199
  #
187
- # @param location [String] The source location of the spec
188
- # @param result [Object] The test result
200
+ # @param location [String] Source location (file:line)
201
+ # @param result [Object] Test execution result
202
+ # @return [void]
189
203
  def report_result(location, result)
190
204
  puts "#{location} #{result.colored_string}"
191
205
  end
192
206
 
193
- # Exits the process with a failure status
207
+ # Exits the process with a failure status.
194
208
  #
195
209
  # @return [void]
196
- # @raise [SystemExit] Always
210
+ # @raise [SystemExit] Always exits with status 1
197
211
  def exit_with_failure
198
212
  ::Kernel.exit(false)
199
213
  end
200
214
 
201
- # Exits the process with the given status
215
+ # Exits the process with the given status.
202
216
  #
203
- # @param status [Boolean] The exit status
217
+ # @param status [Boolean] Exit status to use
204
218
  # @return [void]
205
- # @raise [SystemExit] Always
219
+ # @raise [SystemExit] Always exits with provided status
206
220
  def exit_with_status(status)
207
221
  ::Kernel.exit(status)
208
222
  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
- # Namespace for the Fix framework.
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
- # Provides core functionality for managing and running test specifications.
11
- # Fix supports two modes of operation:
12
- # 1. Named specifications that can be referenced later
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
- # @example Creating and running a named specification
16
- # Fix :Answer do
17
- # it MUST equal 42
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
- # Fix[:Answer].test { 42 }
60
+ # @example Complete specification with multiple matchers
61
+ # Fix :Product do
62
+ # let(:price) { 42.99 }
21
63
  #
22
- # @example Creating and running an anonymous specification
23
- # Fix do
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
- # @see Fix::Set
28
- # @see Fix::Builder
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
- class << self
33
- # Retrieves and loads a built specification for testing.
34
- #
35
- # @example Run a named specification
36
- # Fix[:Answer].test { 42 }
37
- #
38
- # @param name [String, Symbol] The constant name of the specification
39
- # @return [Fix::Set] The loaded specification set ready for testing
40
- # @raise [Fix::Error::SpecificationNotFound] If the named specification doesn't exist
41
- def [](name)
42
- name = normalize_name(name)
43
- validate_specification_exists!(name)
44
- Set.load(name)
45
- end
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
- # Lists all defined specification names.
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
- private
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
- # Converts any specification name into a symbol.
71
- # This allows for consistent name handling regardless of input type.
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
- # Verifies the existence of a specification and raises an error if not found.
83
- # This ensures early failure when attempting to use undefined specifications.
84
- #
85
- # @param name [Symbol] The specification name to validate
86
- # @raise [Fix::Error::SpecificationNotFound] If specification doesn't exist
87
- def validate_specification_exists!(name)
88
- return if spec_defined?(name)
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
- raise Error::SpecificationNotFound, name
91
- end
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 allows Fix to be called from anywhere in the application
7
- # without explicit namespace qualification.
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
- # be referenced later using Fix[name]. Anonymous specifications are executed
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
- # @example Creating a named specification for later use
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 equal 5
42
+ # it MUST eq 5
43
+ # it MUST be_an_instance_of(Integer)
25
44
  # end
26
- # end
27
45
  #
28
- # # Later in the code:
29
- # Fix[:Calculator].test { Calculator.new }
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
- # @param name [String, Symbol, nil] The constant name for the specification
37
- # @yield The specification definition block
38
- # @yieldreturn [void]
39
- # @return [Fix::Set] A collection of specifications ready for testing
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
- # @see Fix::Builder
42
- # @see Fix::Set
43
- # @see Fix::Dsl
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.20'
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-02 00:00:00.000000000 Z
11
+ date: 2025-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: defi
@@ -65,7 +65,6 @@ 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
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