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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e44836a2e867dcb513bc3daffa30c6c319e18602be1c4cf0e098d14f7ae4f9b
4
- data.tar.gz: 70029c70b7efd078c5d4cafdcdacd1921a2c081e1637f7f2cbc867bfde839781
3
+ metadata.gz: a61d6e74ba8d919c935036f602b4ac3bdf4ec121a9880cb6168d16893384e194
4
+ data.tar.gz: 1060db9d560d9288af75686c51bf6acbc38fb29606b3d451493f39548795744f
5
5
  SHA512:
6
- metadata.gz: f18450b4cc887e269471c529e95816537f620e342733a3441091b01709feaf44d7f1fe46e7ea2b228f5eca154f410de0e03456f238aa642e2a35ff1d55aa5376
7
- data.tar.gz: 3854f42a81ddece26f7e9b7d33d347caf37c885cb65ea1e17910032c279c249aefa6ff73d3b7dccfd205d5824812d72564c79d2b76cc7d0afd2e2c51ad08428e
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|
@@ -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
- # Collection of specifications.
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
- # @return [Array] A list of specifications.
14
- attr_reader :specs
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
- # Load specifications from a constant name.
61
+ # Loads a previously registered specification set by name.
17
62
  #
18
- # @param name [String, Symbol] The constant name of the specifications.
19
- # @return [Set] A new Set instance containing the loaded specifications.
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
- # @api public
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
- # Initialize a new Set with given contexts.
75
+ # Initializes a new Set with given test contexts.
27
76
  #
28
- # @param contexts [Array<::Fix::Dsl>] The list of contexts document.
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
- @specs = Doc.extract_specifications(*contexts).shuffle
85
+ @expected = Doc.extract_specifications(*contexts).shuffle
31
86
  end
32
87
 
33
- # Run the test suite against the provided subject.
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
- # @yield The block of code to be tested
36
- # @yieldreturn [Object] The result of the code being tested
37
- # @return [Boolean] true if all tests pass, exits with false otherwise
38
- # @raise [::SystemExit] The test set failed!
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 test(&)
42
- suite_passed?(&) || ::Kernel.exit(false)
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
- private
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
- def suite_passed?(&subject)
48
- specs.all? { |spec| run_spec(*spec, &subject) }
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 { lab(env, location, requirement, challenges, &subject) }
53
- ::Process.wait
54
- $CHILD_STATUS.success?
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
- def lab(env, location, requirement, challenges, &)
58
- result = Run.new(env, requirement, *challenges).test(&)
59
- report!(location, result)
60
- ::Kernel.exit(result.passed?)
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
- def report!(path, result)
64
- puts "#{path} #{result.colored_string}"
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
- # 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.19'
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-01 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,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