fix 0.21 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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