fix 0.21 → 1.0.0.beta1

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.
data/lib/fix/dsl.rb DELETED
@@ -1,139 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "defi/method"
4
-
5
- require_relative "matcher"
6
- require_relative "requirement"
7
-
8
- module Fix
9
- # Abstract class for handling the domain-specific language.
10
- #
11
- # @api private
12
- class Dsl
13
- extend Matcher
14
- extend Requirement
15
-
16
- # Sets a user-defined property.
17
- #
18
- # @example
19
- # require "fix"
20
- #
21
- # Fix do
22
- # let(:name) { "Bob" }
23
- # end
24
- #
25
- # @param name [String, Symbol] The name of the property.
26
- # @yield The block that defines the property's value
27
- # @yieldreturn [Object] The value to be returned by the property
28
- #
29
- # @return [Symbol] A private method that defines the block content.
30
- #
31
- # @api public
32
- def self.let(name, &)
33
- private define_method(name, &)
34
- end
35
-
36
- # Defines an example group with user-defined properties that describes a
37
- # unit to be tested.
38
- #
39
- # @example
40
- # require "fix"
41
- #
42
- # Fix do
43
- # with password: "secret" do
44
- # it MUST be true
45
- # end
46
- # end
47
- #
48
- # @param kwargs [Hash] The list of properties to define in this context
49
- # @yield The block that defines the specs for this context
50
- # @yieldreturn [void]
51
- #
52
- # @return [Class] A new class representing this context
53
- #
54
- # @api public
55
- def self.with(**kwargs, &)
56
- klass = ::Class.new(self)
57
- klass.const_get(:CONTEXTS) << klass
58
- kwargs.each { |name, value| klass.let(name) { value } }
59
- klass.instance_eval(&)
60
- klass
61
- end
62
-
63
- # Defines an example group that describes a unit to be tested.
64
- #
65
- # @example
66
- # require "fix"
67
- #
68
- # Fix do
69
- # on :+, 2 do
70
- # it MUST be 42
71
- # end
72
- # end
73
- #
74
- # @param method_name [String, Symbol] The method to send to the subject
75
- # @param args [Array] Positional arguments to pass to the method
76
- # @param kwargs [Hash] Keyword arguments to pass to the method
77
- # @yield The block containing the specifications for this context
78
- # @yieldreturn [void]
79
- #
80
- # @return [Class] A new class representing this context
81
- #
82
- # @api public
83
- def self.on(method_name, *args, **kwargs, &block)
84
- klass = ::Class.new(self)
85
- klass.const_get(:CONTEXTS) << klass
86
-
87
- const_name = :"MethodContext_#{block.object_id}"
88
- const_set(const_name, klass)
89
-
90
- klass.define_singleton_method(:challenges) do
91
- challenge = ::Defi::Method.new(method_name, *args, **kwargs)
92
- super() + [challenge]
93
- end
94
-
95
- klass.instance_eval(&block)
96
- klass
97
- end
98
-
99
- # Defines a concrete spec definition.
100
- #
101
- # @example
102
- # require "fix"
103
- #
104
- # Fix { it MUST be 42 }
105
- #
106
- # Fix do
107
- # it { MUST be 42 }
108
- # end
109
- #
110
- # @param requirement [Object, nil] The requirement to test
111
- # @yield A block defining the requirement if not provided directly
112
- # @yieldreturn [Object] The requirement definition
113
- #
114
- # @return [Symbol] Name of the generated test method
115
- #
116
- # @raise [ArgumentError] If neither or both requirement and block are provided
117
- #
118
- # @api public
119
- def self.it(requirement = nil, &block)
120
- raise ::ArgumentError, "Must provide either requirement or block, not both" if requirement && block
121
- raise ::ArgumentError, "Must provide either requirement or block" unless requirement || block
122
-
123
- location = caller_locations(1, 1).fetch(0)
124
- location = [location.path, location.lineno].join(":")
125
-
126
- test_method_name = :"test_#{(requirement || block).object_id}"
127
- define_method(test_method_name) do
128
- [location, requirement || singleton_class.class_eval(&block), self.class.challenges]
129
- end
130
- end
131
-
132
- # The list of challenges to be addressed to the object to be tested.
133
- #
134
- # @return [Array<Defi::Method>] A list of challenges.
135
- def self.challenges
136
- []
137
- end
138
- end
139
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fix
4
- module Error
5
- # Error raised when an invalid specification name is provided during declaration
6
- class InvalidSpecificationName < ::NameError
7
- def initialize(name)
8
- super("Invalid specification name '#{name}'. Specification names must be valid Ruby constants.")
9
- end
10
- end
11
- end
12
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fix
4
- module Error
5
- # Error raised when attempting to build a specification without a block
6
- class MissingSpecificationBlock < ::ArgumentError
7
- MISSING_BLOCK_ERROR = "Block is required for building a specification"
8
-
9
- def initialize
10
- super(MISSING_BLOCK_ERROR)
11
- end
12
- end
13
- end
14
- end
@@ -1,15 +0,0 @@
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
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fix
4
- module Error
5
- # Error raised when a specification cannot be found at runtime
6
- class SpecificationNotFound < ::NameError
7
- def initialize(name)
8
- super("Specification '#{name}' not found. Make sure it's defined before running the test.")
9
- end
10
- end
11
- end
12
- end
data/lib/fix/matcher.rb DELETED
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "matchi"
4
-
5
- module Fix
6
- # Collection of expectation matchers.
7
- # Provides a comprehensive set of matchers for testing different aspects of objects.
8
- #
9
- # The following matchers are available:
10
- #
11
- # Basic Comparison:
12
- # - eq(expected) # Checks equality using eql?
13
- # it MUST eq(42)
14
- # it MUST eq("hello")
15
- # - eql(expected) # Alias for eq
16
- # - be(expected) # Checks exact object identity using equal?
17
- # string = "test"
18
- # it MUST be(string) # Passes only if it's the same object
19
- # - equal(expected) # Alias for be
20
- #
21
- # Type Checking:
22
- # - be_an_instance_of(class) # Checks exact class match
23
- # it MUST be_an_instance_of(Array)
24
- # - be_a_kind_of(class) # Checks class inheritance and module inclusion
25
- # it MUST be_a_kind_of(Enumerable)
26
- #
27
- # State & Changes:
28
- # - change(object, method) # Base for checking state changes
29
- # .by(n) # Exact change by n
30
- # it MUST change(user, :points).by(5)
31
- # .by_at_least(n) # Minimum change by n
32
- # it MUST change(counter, :value).by_at_least(10)
33
- # .by_at_most(n) # Maximum change by n
34
- # it MUST change(account, :balance).by_at_most(100)
35
- # .from(old).to(new) # Change from old to new value
36
- # it MUST change(user, :status).from("pending").to("active")
37
- # .to(new) # Change to new value
38
- # it MUST change(post, :title).to("Updated")
39
- #
40
- # Value Testing:
41
- # - be_within(delta).of(value) # Checks numeric value within delta
42
- # it MUST be_within(0.1).of(3.14)
43
- # - match(regex) # Tests against regular expression
44
- # it MUST match(/^\d{3}-\d{2}-\d{4}$/) # SSN format
45
- # - satisfy { |value| ... } # Custom matcher with block
46
- # it MUST satisfy { |num| num.even? && num > 0 }
47
- #
48
- # Exceptions:
49
- # - raise_exception(class) # Checks if code raises exception
50
- # it MUST raise_exception(ArgumentError)
51
- # it MUST raise_exception(CustomError, "specific message")
52
- #
53
- # State Testing:
54
- # - be_true # Tests for true
55
- # it MUST be_true # Only passes for true, not truthy values
56
- # - be_false # Tests for false
57
- # it MUST be_false # Only passes for false, not falsey values
58
- # - be_nil # Tests for nil
59
- # it MUST be_nil
60
- #
61
- # Predicate Matchers:
62
- # - be_* # Matches object.*? method
63
- # it MUST be_empty # Calls empty?
64
- # it MUST be_valid # Calls valid?
65
- # it MUST be_frozen # Calls frozen?
66
- # - have_* # Matches object.has_*? method
67
- # it MUST have_key(:id) # Calls has_key?
68
- # it MUST have_errors # Calls has_errors?
69
- #
70
- # @note All matchers can be used with MUST, MUST_NOT, SHOULD, SHOULD_NOT, and MAY
71
- # @see https://github.com/fixrb/matchi for more details about the matchers
72
- # @api private
73
- module Matcher
74
- include Matchi
75
- end
76
- end
@@ -1,119 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spectus/requirement/optional"
4
- require "spectus/requirement/recommended"
5
- require "spectus/requirement/required"
6
-
7
- module Fix
8
- # Implements requirement levels as defined in RFC 2119.
9
- # Provides methods for specifying different levels of requirements
10
- # in test specifications: MUST, SHOULD, and MAY.
11
- #
12
- # @api private
13
- module Requirement
14
- # rubocop:disable Naming/MethodName
15
-
16
- # This method means that the definition is an absolute requirement of the
17
- # specification.
18
- #
19
- # @example Test exact equality
20
- # it MUST eq(42)
21
- #
22
- # @example Test type matching
23
- # it MUST be_an_instance_of(User)
24
- #
25
- # @example Test state changes
26
- # it MUST change(user, :status).from("pending").to("active")
27
- #
28
- # @param matcher [#match?] The matcher that defines the required condition
29
- # @return [::Spectus::Requirement::Required] An absolute requirement level instance
30
- #
31
- # @api public
32
- def MUST(matcher)
33
- ::Spectus::Requirement::Required.new(negate: false, matcher:)
34
- end
35
-
36
- # This method means that the definition is an absolute prohibition of the
37
- # specification.
38
- #
39
- # @example Test prohibited state
40
- # it MUST_NOT be_nil
41
- #
42
- # @example Test prohibited type
43
- # it MUST_NOT be_a_kind_of(AdminUser)
44
- #
45
- # @example Test prohibited exception
46
- # it MUST_NOT raise_exception(SecurityError)
47
- #
48
- # @param matcher [#match?] The matcher that defines the prohibited condition
49
- # @return [::Spectus::Requirement::Required] An absolute prohibition level instance
50
- #
51
- # @api public
52
- def MUST_NOT(matcher)
53
- ::Spectus::Requirement::Required.new(negate: true, matcher:)
54
- end
55
-
56
- # This method means that there may exist valid reasons in particular
57
- # circumstances to ignore this requirement, but the implications must be
58
- # understood and carefully weighed.
59
- #
60
- # @example Test numeric boundaries
61
- # it SHOULD be_within(0.1).of(expected_value)
62
- #
63
- # @example Test pattern matching
64
- # it SHOULD match(/^[A-Z][a-z]+$/)
65
- #
66
- # @example Test custom condition
67
- # it SHOULD satisfy { |obj| obj.valid? && obj.complete? }
68
- #
69
- # @param matcher [#match?] The matcher that defines the recommended condition
70
- # @return [::Spectus::Requirement::Recommended] A recommended requirement level instance
71
- #
72
- # @api public
73
- def SHOULD(matcher)
74
- ::Spectus::Requirement::Recommended.new(negate: false, matcher:)
75
- end
76
-
77
- # This method means that there may exist valid reasons in particular
78
- # circumstances when the behavior is acceptable, but the implications should be
79
- # understood and weighed carefully.
80
- #
81
- # @example Test state changes to avoid
82
- # it SHOULD_NOT change(object, :state)
83
- #
84
- # @example Test predicate conditions to avoid
85
- # it SHOULD_NOT be_empty
86
- # it SHOULD_NOT have_errors
87
- #
88
- # @param matcher [#match?] The matcher that defines the discouraged condition
89
- # @return [::Spectus::Requirement::Recommended] A discouraged requirement level instance
90
- #
91
- # @api public
92
- def SHOULD_NOT(matcher)
93
- ::Spectus::Requirement::Recommended.new(negate: true, matcher:)
94
- end
95
-
96
- # This method means that the item is truly optional. Implementations may
97
- # include this feature if it enhances their product, and must be prepared to
98
- # interoperate with implementations that include or omit this feature.
99
- #
100
- # @example Test optional functionality
101
- # it MAY respond_to(:cache_key)
102
- #
103
- # @example Test optional state
104
- # it MAY be_frozen
105
- #
106
- # @example Test optional predicates
107
- # it MAY have_attachments
108
- #
109
- # @param matcher [#match?] The matcher that defines the optional condition
110
- # @return [::Spectus::Requirement::Optional] An optional requirement level instance
111
- #
112
- # @api public
113
- def MAY(matcher)
114
- ::Spectus::Requirement::Optional.new(negate: false, matcher:)
115
- end
116
-
117
- # rubocop:enable Naming/MethodName
118
- end
119
- end
data/lib/fix/run.rb DELETED
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "expresenter/fail"
4
-
5
- module Fix
6
- # Executes a test specification by running a subject against a set of challenges
7
- # and requirements.
8
- #
9
- # The Run class orchestrates test execution by:
10
- # 1. Evaluating the test subject in the proper environment
11
- # 2. Applying a series of method challenges to the result
12
- # 3. Verifying the final value against the requirement
13
- #
14
- # @example Running a simple test
15
- # run = Run.new(env, requirement)
16
- # run.test { MyClass.new }
17
- #
18
- # @example Running with method challenges
19
- # run = Run.new(env, requirement, challenge1, challenge2)
20
- # run.test { MyClass.new } # Will call methods defined in challenges
21
- #
22
- # @api private
23
- class Run
24
- # The test environment containing defined variables and methods
25
- # @return [::Fix::Dsl] A context instance
26
- attr_reader :environment
27
-
28
- # The specification requirement to validate against
29
- # @return [::Spectus::Requirement::Base] An expectation
30
- attr_reader :requirement
31
-
32
- # The list of method calls to apply to the subject
33
- # @return [Array<::Defi::Method>] A list of challenges
34
- attr_reader :challenges
35
-
36
- # Initializes a new test run with the given environment and challenges.
37
- #
38
- # @param environment [::Fix::Dsl] Context instance with test setup
39
- # @param requirement [::Spectus::Requirement::Base] Expectation to verify
40
- # @param challenges [Array<::Defi::Method>] Method calls to apply
41
- #
42
- # @example
43
- # Run.new(test_env, must_be_positive, increment_method)
44
- def initialize(environment, requirement, *challenges)
45
- @environment = environment
46
- @requirement = requirement
47
- @challenges = challenges
48
- end
49
-
50
- # Verifies if the subject meets the requirement after applying all challenges.
51
- #
52
- # @param subject [Proc] The block of code to be tested
53
- #
54
- # @raise [::Expresenter::Fail] When the test specification fails
55
- # @return [::Expresenter::Pass] When the test specification passes
56
- #
57
- # @example Basic testing
58
- # run.test { 42 }
59
- #
60
- # @example Testing with subject modification
61
- # run.test { User.new(name: "John") }
62
- #
63
- # @see https://github.com/fixrb/expresenter
64
- def test(&subject)
65
- requirement.call { actual_value(&subject) }
66
- rescue ::Expresenter::Fail => e
67
- e
68
- end
69
-
70
- private
71
-
72
- # Computes the final value to test by applying all challenges to the subject.
73
- #
74
- # @param subject [Proc] The initial test subject
75
- # @return [#object_id] The final value after applying all challenges
76
- #
77
- # @example Internal process
78
- # # If challenges are [:upcase, :reverse]
79
- # # and subject returns "hello"
80
- # # actual_value will return "OLLEH"
81
- def actual_value(&subject)
82
- initial_value = environment.instance_eval(&subject)
83
- challenges.inject(initial_value) do |obj, challenge|
84
- challenge.to(obj).call
85
- end
86
- end
87
- end
88
- end