fix 0.20 → 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.
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