fix 1.0.0.beta11 → 1.0.0
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 +4 -4
- data/LICENSE.md +2 -2
- data/README.md +189 -155
- data/lib/fix/dsl.rb +149 -97
- data/lib/fix/environment.rb +25 -0
- data/lib/fix/level.rb +40 -0
- data/lib/fix/matcher.rb +50 -191
- data/lib/fix/node.rb +32 -0
- data/lib/fix/result.rb +36 -0
- data/lib/fix/spec.rb +167 -0
- data/lib/fix.rb +42 -12
- data/lib/kernel.rb +10 -31
- metadata +35 -19
- data/lib/fix/doc.rb +0 -24
- data/lib/fix/requirement.rb +0 -87
- data/lib/fix/run.rb +0 -55
- data/lib/fix/set.rb +0 -67
data/lib/fix/dsl.rb
CHANGED
|
@@ -1,122 +1,174 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "defi
|
|
3
|
+
require "defi"
|
|
4
|
+
require "spectus"
|
|
4
5
|
|
|
6
|
+
require_relative "environment"
|
|
7
|
+
require_relative "level"
|
|
5
8
|
require_relative "matcher"
|
|
6
|
-
require_relative "
|
|
9
|
+
require_relative "node"
|
|
7
10
|
|
|
8
11
|
module Fix
|
|
9
|
-
#
|
|
12
|
+
# The language of a specification. The block of `Fix :Name do … end` runs
|
|
13
|
+
# on an instance of this class, thrown away afterwards, whose four words
|
|
14
|
+
# build the contexts of the specification: `let`, `with`, `on` and `it`.
|
|
15
|
+
# The matchers ({Fix::Matcher}) and the levels ({Fix::Level}) are words of
|
|
16
|
+
# it too.
|
|
10
17
|
#
|
|
11
|
-
#
|
|
18
|
+
# A gem may add a word that composes these four: fix-its adds `its`.
|
|
12
19
|
class Dsl
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
include Level
|
|
21
|
+
include Matcher
|
|
15
22
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 define the block content.
|
|
30
|
-
#
|
|
31
|
-
# @api public
|
|
32
|
-
def self.let(name, &)
|
|
33
|
-
private define_method(name, &)
|
|
23
|
+
# The four words.
|
|
24
|
+
WORDS = %i[let with on it].freeze
|
|
25
|
+
private_constant :WORDS
|
|
26
|
+
|
|
27
|
+
# @yield the document of a specification
|
|
28
|
+
# @return [Array<Fix::Node>] its contexts, each before its children
|
|
29
|
+
def self.build(&)
|
|
30
|
+
dsl = new({}.freeze, [].freeze, "")
|
|
31
|
+
dsl.instance_exec(&)
|
|
32
|
+
dsl.nodes.freeze
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# @
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# @yield The block that defines the specs for this context
|
|
50
|
-
# @yieldreturn [void]
|
|
51
|
-
#
|
|
52
|
-
# @api public
|
|
53
|
-
def self.with(**kwargs, &)
|
|
54
|
-
klass = ::Class.new(self)
|
|
55
|
-
klass.const_get(:CONTEXTS) << klass
|
|
56
|
-
kwargs.each { |name, value| klass.let(name) { value } }
|
|
57
|
-
klass.instance_eval(&)
|
|
58
|
-
klass
|
|
35
|
+
# @param lets [Hash{Symbol => Proc}] the lets inherited, frozen
|
|
36
|
+
# @param challenges [Array<Defi::Challenge>] the chain inherited, frozen
|
|
37
|
+
# @param chain [String] its text
|
|
38
|
+
# @param own [Array<Symbol>] the lets this context declares already,
|
|
39
|
+
# those a `with` gave it
|
|
40
|
+
# @api private
|
|
41
|
+
def initialize(lets, challenges, chain, own = [])
|
|
42
|
+
@lets = lets
|
|
43
|
+
@challenges = challenges
|
|
44
|
+
@chain = chain
|
|
45
|
+
@own = own
|
|
46
|
+
@expectations = []
|
|
47
|
+
@children = []
|
|
59
48
|
end
|
|
60
49
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# on :+, 2 do
|
|
68
|
-
# it MUST be 42
|
|
69
|
-
# end
|
|
70
|
-
# end
|
|
71
|
-
#
|
|
72
|
-
# @param method_name [String, Symbol] The method to send to the subject.
|
|
73
|
-
# @param block [Proc] The block to define the specs.
|
|
74
|
-
#
|
|
75
|
-
# @api public
|
|
76
|
-
def self.on(method_name, *args, **kwargs, &block)
|
|
77
|
-
klass = ::Class.new(self)
|
|
78
|
-
klass.const_get(:CONTEXTS) << klass
|
|
50
|
+
# @return [Array<Fix::Node>] this context, then its children, as they
|
|
51
|
+
# stand
|
|
52
|
+
def nodes
|
|
53
|
+
node = Node.new(lets: @lets, challenges: @challenges, chain: @chain, expectations: @expectations.dup.freeze)
|
|
54
|
+
[node] + @children
|
|
55
|
+
end
|
|
79
56
|
|
|
80
|
-
|
|
57
|
+
# A parameter of the subject: its block runs where the subject is built,
|
|
58
|
+
# once per expectation, and may call other lets. A let is not visible
|
|
59
|
+
# from an expectation.
|
|
60
|
+
#
|
|
61
|
+
# @param name [Symbol] its name, which is not a method of the environment
|
|
62
|
+
# nor a word of the language, and not a let of this context already
|
|
63
|
+
# @yieldreturn [Object] its value
|
|
64
|
+
# @return [nil]
|
|
65
|
+
# @raise [ArgumentError] otherwise, or without a block
|
|
66
|
+
def let(name, &block)
|
|
67
|
+
raise ::ArgumentError, "a block must be given" unless block
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
super() + [challenge]
|
|
85
|
-
end
|
|
69
|
+
free!(name)
|
|
70
|
+
raise ::ArgumentError, "#{name} is already a let of this context" if @own.include?(name)
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
@own << name
|
|
73
|
+
@lets = @lets.merge(name => block).freeze
|
|
74
|
+
nil
|
|
89
75
|
end
|
|
90
76
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# @api public
|
|
103
|
-
def self.it(requirement = nil, &block)
|
|
104
|
-
raise ::ArgumentError, "Must provide either requirement or block, not both" if requirement && block
|
|
105
|
-
raise ::ArgumentError, "Must provide either requirement or block" unless requirement || block
|
|
77
|
+
# A context where lets take the values given: the objects themselves,
|
|
78
|
+
# shared by the expectations of the context, which declares these lets.
|
|
79
|
+
#
|
|
80
|
+
# @param values [Hash{Symbol => Object}] the lets, by name
|
|
81
|
+
# @yield the context
|
|
82
|
+
# @return [nil]
|
|
83
|
+
# @raise [ArgumentError] for a name a let could not take, without a value
|
|
84
|
+
# or without a block
|
|
85
|
+
def with(**values, &block)
|
|
86
|
+
raise ::ArgumentError, "a block must be given" unless block
|
|
87
|
+
raise ::ArgumentError, "a with gives a value to a let: with(name: 1)" if values.empty?
|
|
106
88
|
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
values.each_key { |name| free!(name) }
|
|
90
|
+
lets = @lets.merge(values.transform_values { |value| -> { value } }).freeze
|
|
91
|
+
context(lets, @challenges, @chain, values.keys, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A context about what a method of the subject returns, or raises: the
|
|
95
|
+
# method is sent, with its arguments, to the value of the context above.
|
|
96
|
+
# No block can be given to the method: the block is the context.
|
|
97
|
+
#
|
|
98
|
+
# @param name [Symbol] the method, followed by its positional and keyword
|
|
99
|
+
# arguments, as they would be written after it
|
|
100
|
+
# @yield the context
|
|
101
|
+
# @return [nil]
|
|
102
|
+
# @raise [ArgumentError] when the name is not a Symbol, or without a block
|
|
103
|
+
def on(name, *, **, &block)
|
|
104
|
+
raise ::ArgumentError, "a block must be given" unless block
|
|
109
105
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
end
|
|
106
|
+
challenge = ::Defi::Challenge.new(name, *, **)
|
|
107
|
+
context(@lets, (@challenges + [challenge]).freeze, text("#{@chain}#{challenge}"), &block)
|
|
113
108
|
end
|
|
114
109
|
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
# An expectation: a requirement, built here and now with a level and a
|
|
111
|
+
# matcher, at the line of the call.
|
|
112
|
+
#
|
|
113
|
+
# @param requirement [Spectus::Requirement] `MUST eq(1)`, say
|
|
114
|
+
# @param location [Thread::Backtrace::Location, nil] where the expectation
|
|
115
|
+
# is written, for a word that composes `it`; the caller of `it` otherwise
|
|
116
|
+
# @return [nil]
|
|
117
|
+
# @raise [ArgumentError] without a requirement, or with a block
|
|
118
|
+
# @raise [TypeError] when the `to_s` of the matcher is not a String
|
|
119
|
+
def it(requirement = nil, location: nil, &block)
|
|
120
|
+
raise ::ArgumentError, "it takes a requirement, not a block: it MUST eq(1)" if block
|
|
121
|
+
raise ::ArgumentError, "it takes a requirement: it MUST eq(1)" unless ::Spectus::Requirement === requirement
|
|
122
|
+
raise ::TypeError, "the to_s of the matcher must be a String" unless ::String === requirement.matcher.to_s
|
|
123
|
+
|
|
124
|
+
location ||= caller_locations(1, 1).fetch(0)
|
|
125
|
+
@expectations << Expectation.new(location: locate(location), requirement:)
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# A let named in an expectation is a mistake with a remedy.
|
|
132
|
+
def method_missing(name, ...)
|
|
133
|
+
return super unless @lets.key?(name)
|
|
134
|
+
|
|
135
|
+
raise ::NameError.new("#{name} is a let, visible in the subject block and in other lets only", name)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A child context, whose nodes follow this one's.
|
|
143
|
+
def context(lets, challenges, chain, own = [], &)
|
|
144
|
+
child = Dsl.new(lets, challenges, chain, own)
|
|
145
|
+
child.instance_exec(&)
|
|
146
|
+
@children.concat(child.nodes)
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A name a let can take: a Symbol that is not a method of the environment,
|
|
151
|
+
# public or private, nor a public word of the language, so that nothing
|
|
152
|
+
# is redefined in silence.
|
|
153
|
+
def free!(name)
|
|
154
|
+
raise ::ArgumentError, "the name of a let must be a Symbol" unless ::Symbol === name
|
|
155
|
+
raise ::ArgumentError, "#{name} is a word of the specification language" if WORDS.include?(name)
|
|
156
|
+
|
|
157
|
+
owner = [Environment, Dsl].find { |klass| klass.method_defined?(name) }
|
|
158
|
+
owner ||= Environment if Environment.private_method_defined?(name)
|
|
159
|
+
raise ::ArgumentError, "#{name} is already a method of #{owner.instance_method(name).owner}" if owner
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `file:line`, the file relative to the current directory when it is
|
|
163
|
+
# under it.
|
|
164
|
+
def locate(location)
|
|
165
|
+
path = ::File.absolute_path(location.path).delete_prefix(::File.join(::Dir.pwd, ""))
|
|
166
|
+
text("#{path}:#{location.lineno}")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A text of a result, valid UTF-8 and frozen, as those of Spectus.
|
|
170
|
+
def text(string)
|
|
171
|
+
string.encode(::Encoding::UTF_8, invalid: :replace, undef: :replace).freeze
|
|
120
172
|
end
|
|
121
173
|
end
|
|
122
174
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fix
|
|
4
|
+
# Where a subject is built: an object with a method for each let, whose
|
|
5
|
+
# value is computed once, the first time it is called, by running the
|
|
6
|
+
# block of the let here, so that a let can call another. A new environment
|
|
7
|
+
# is made for each expectation. It is an ordinary object, so that the
|
|
8
|
+
# blocks of a specification can call `Kernel` and the private methods of
|
|
9
|
+
# the top level, as any Ruby code does.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Environment
|
|
13
|
+
# @param lets [Hash{Symbol => Proc}] the lets of a context
|
|
14
|
+
def initialize(lets)
|
|
15
|
+
values = {}
|
|
16
|
+
lets.each do |name, block|
|
|
17
|
+
define_singleton_method(name) do
|
|
18
|
+
values.fetch(name) { values[name] = instance_exec(&block) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private_constant :Environment
|
|
25
|
+
end
|
data/lib/fix/level.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spectus"
|
|
4
|
+
|
|
5
|
+
module Fix
|
|
6
|
+
# The requirement levels of RFC 2119, as the words of a specification:
|
|
7
|
+
# each one turns a matcher into a `Spectus::Requirement`. A MAY has no
|
|
8
|
+
# negative form.
|
|
9
|
+
module Level
|
|
10
|
+
# @param matcher [#match?] the matcher
|
|
11
|
+
# @return [Spectus::Requirement] an absolute requirement
|
|
12
|
+
def MUST(matcher) # rubocop:disable Naming/MethodName
|
|
13
|
+
::Spectus.must(matcher)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param matcher [#match?] the matcher
|
|
17
|
+
# @return [Spectus::Requirement] an absolute prohibition
|
|
18
|
+
def MUST_NOT(matcher) # rubocop:disable Naming/MethodName
|
|
19
|
+
::Spectus.must_not(matcher)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param matcher [#match?] the matcher
|
|
23
|
+
# @return [Spectus::Requirement] a recommendation
|
|
24
|
+
def SHOULD(matcher) # rubocop:disable Naming/MethodName
|
|
25
|
+
::Spectus.should(matcher)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param matcher [#match?] the matcher
|
|
29
|
+
# @return [Spectus::Requirement] a negated recommendation
|
|
30
|
+
def SHOULD_NOT(matcher) # rubocop:disable Naming/MethodName
|
|
31
|
+
::Spectus.should_not(matcher)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param matcher [#match?] the matcher
|
|
35
|
+
# @return [Spectus::Requirement] an option
|
|
36
|
+
def MAY(matcher) # rubocop:disable Naming/MethodName
|
|
37
|
+
::Spectus.may(matcher)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/fix/matcher.rb
CHANGED
|
@@ -3,239 +3,98 @@
|
|
|
3
3
|
require "matchi"
|
|
4
4
|
|
|
5
5
|
module Fix
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# The matchers of a specification, as words: each one builds a matcher of
|
|
7
|
+
# Matchi, or returns a specification, which is a matcher too, and raises
|
|
8
|
+
# `ArgumentError` at load for an argument the matcher refuses. Any object
|
|
9
|
+
# with `match?` and `to_s` is accepted by the levels as well.
|
|
9
10
|
module Matcher
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# @example
|
|
13
|
-
# matcher = eq("foo")
|
|
14
|
-
# matcher.match? { "foo" } # => true
|
|
15
|
-
# matcher.match? { "bar" } # => false
|
|
16
|
-
#
|
|
17
|
-
# @param expected [#eql?] An expected equivalent object.
|
|
18
|
-
#
|
|
19
|
-
# @return [#match?] An equivalence matcher.
|
|
20
|
-
#
|
|
21
|
-
# @api public
|
|
11
|
+
# @param expected [Object] the expected value
|
|
12
|
+
# @return [Matchi::Eq] `eql?` to the expected value, of its class
|
|
22
13
|
def eq(expected)
|
|
23
14
|
::Matchi::Eq.new(expected)
|
|
24
15
|
end
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# Identity matcher
|
|
29
|
-
#
|
|
30
|
-
# @example
|
|
31
|
-
# object = "foo"
|
|
32
|
-
# matcher = be(object)
|
|
33
|
-
# matcher.match? { object } # => true
|
|
34
|
-
# matcher.match? { "foo" } # => false
|
|
35
|
-
#
|
|
36
|
-
# @param expected [#equal?] The expected identical object.
|
|
37
|
-
#
|
|
38
|
-
# @return [#match?] An identity matcher.
|
|
39
|
-
#
|
|
40
|
-
# @api public
|
|
17
|
+
# @param expected [Object] the expected object
|
|
18
|
+
# @return [Matchi::Be] the very same object
|
|
41
19
|
def be(expected)
|
|
42
20
|
::Matchi::Be.new(expected)
|
|
43
21
|
end
|
|
44
22
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Comparisons matcher
|
|
48
|
-
#
|
|
49
|
-
# @example
|
|
50
|
-
# matcher = be_within(1).of(41)
|
|
51
|
-
# matcher.match? { 42 } # => true
|
|
52
|
-
# matcher.match? { 43 } # => false
|
|
53
|
-
#
|
|
54
|
-
# @param delta [Numeric] A numeric value.
|
|
55
|
-
#
|
|
56
|
-
# @return [#match?] A comparison matcher.
|
|
57
|
-
#
|
|
58
|
-
# @api public
|
|
23
|
+
# @param delta [Numeric] the tolerance
|
|
24
|
+
# @return [Matchi::BeWithin] to complete with `of(expected)`
|
|
59
25
|
def be_within(delta)
|
|
60
26
|
::Matchi::BeWithin.new(delta)
|
|
61
27
|
end
|
|
62
28
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# @example
|
|
66
|
-
# matcher = match(/^foo$/)
|
|
67
|
-
# matcher.match? { "foo" } # => true
|
|
68
|
-
# matcher.match? { "bar" } # => false
|
|
69
|
-
#
|
|
70
|
-
# @param expected [#match] A regular expression.
|
|
71
|
-
#
|
|
72
|
-
# @return [#match?] A regular expression matcher.
|
|
73
|
-
#
|
|
74
|
-
# @api public
|
|
29
|
+
# @param expected [Regexp] the pattern
|
|
30
|
+
# @return [Matchi::Match] a String or a Symbol matching the pattern
|
|
75
31
|
def match(expected)
|
|
76
32
|
::Matchi::Match.new(expected)
|
|
77
33
|
end
|
|
78
34
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# @
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# matcher.match? { true } # => false
|
|
85
|
-
#
|
|
86
|
-
# @param expected [Exception, #to_s] The expected exception name.
|
|
87
|
-
#
|
|
88
|
-
# @return [#match?] An error matcher.
|
|
89
|
-
#
|
|
90
|
-
# @api public
|
|
91
|
-
def raise_exception(expected)
|
|
92
|
-
::Matchi::RaiseException.new(expected)
|
|
35
|
+
# @param expected [Class, String, Symbol] the exception class, or its name
|
|
36
|
+
# @param message [String, Regexp, nil] the expected message
|
|
37
|
+
# @return [Matchi::RaiseException] raising that exception
|
|
38
|
+
def raise_exception(expected, message = nil)
|
|
39
|
+
::Matchi::RaiseException.new(expected, message)
|
|
93
40
|
end
|
|
94
41
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# @example
|
|
98
|
-
# matcher = be_true
|
|
99
|
-
# matcher.match? { true } # => true
|
|
100
|
-
# matcher.match? { false } # => false
|
|
101
|
-
# matcher.match? { nil } # => false
|
|
102
|
-
# matcher.match? { 4 } # => false
|
|
103
|
-
#
|
|
104
|
-
# @return [#match?] A `true` matcher.
|
|
105
|
-
#
|
|
106
|
-
# @api public
|
|
42
|
+
# @return [Matchi::Be] `true` itself
|
|
107
43
|
def be_true
|
|
108
|
-
|
|
44
|
+
::Matchi::Be.new(true)
|
|
109
45
|
end
|
|
110
46
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# @example
|
|
114
|
-
# matcher = be_false
|
|
115
|
-
# matcher.match? { false } # => true
|
|
116
|
-
# matcher.match? { true } # => false
|
|
117
|
-
# matcher.match? { nil } # => false
|
|
118
|
-
# matcher.match? { 4 } # => false
|
|
119
|
-
#
|
|
120
|
-
# @return [#match?] A `false` matcher.
|
|
121
|
-
#
|
|
122
|
-
# @api public
|
|
47
|
+
# @return [Matchi::Be] `false` itself
|
|
123
48
|
def be_false
|
|
124
|
-
|
|
49
|
+
::Matchi::Be.new(false)
|
|
125
50
|
end
|
|
126
51
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# @example
|
|
130
|
-
# matcher = be_nil
|
|
131
|
-
# matcher.match? { nil } # => true
|
|
132
|
-
# matcher.match? { false } # => false
|
|
133
|
-
# matcher.match? { true } # => false
|
|
134
|
-
# matcher.match? { 4 } # => false
|
|
135
|
-
#
|
|
136
|
-
# @return [#match?] A `nil` matcher.
|
|
137
|
-
#
|
|
138
|
-
# @api public
|
|
52
|
+
# @return [Matchi::Be] `nil` itself
|
|
139
53
|
def be_nil
|
|
140
|
-
|
|
54
|
+
::Matchi::Be.new(nil)
|
|
141
55
|
end
|
|
142
56
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# @example
|
|
146
|
-
# matcher = be_an_instance_of(String)
|
|
147
|
-
# matcher.match? { "foo" } # => true
|
|
148
|
-
# matcher.match? { 4 } # => false
|
|
149
|
-
#
|
|
150
|
-
# @param expected [Class, #to_s] The expected class name.
|
|
151
|
-
#
|
|
152
|
-
# @return [#match?] A type/class matcher.
|
|
153
|
-
#
|
|
154
|
-
# @api public
|
|
57
|
+
# @param expected [Class, String, Symbol] the class, or its name
|
|
58
|
+
# @return [Matchi::BeAnInstanceOf] an instance of that very class
|
|
155
59
|
def be_an_instance_of(expected)
|
|
156
60
|
::Matchi::BeAnInstanceOf.new(expected)
|
|
157
61
|
end
|
|
158
62
|
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
# matcher = change(object, :length).by_at_least(1)
|
|
168
|
-
# matcher.match? { object << 1 } # => true
|
|
169
|
-
#
|
|
170
|
-
# object = []
|
|
171
|
-
# matcher = change(object, :length).by_at_most(1)
|
|
172
|
-
# matcher.match? { object << 1 } # => true
|
|
173
|
-
#
|
|
174
|
-
# object = "foo"
|
|
175
|
-
# matcher = change(object, :to_s).from("foo").to("FOO")
|
|
176
|
-
# matcher.match? { object.upcase! } # => true
|
|
177
|
-
#
|
|
178
|
-
# object = "foo"
|
|
179
|
-
# matcher = change(object, :to_s).to("FOO")
|
|
180
|
-
# matcher.match? { object.upcase! } # => true
|
|
181
|
-
#
|
|
182
|
-
# @param object [#object_id] An object.
|
|
183
|
-
# @param method [Symbol] The name of a method.
|
|
184
|
-
#
|
|
185
|
-
# @return [#match?] A change matcher.
|
|
63
|
+
# @param expected [Module, String, Symbol] the module, or its name
|
|
64
|
+
# @return [Matchi::BeAKindOf] an instance of that module or of a descendant
|
|
65
|
+
def be_a_kind_of(expected)
|
|
66
|
+
::Matchi::BeAKindOf.new(expected)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The object must exist when the specification is loaded: a let, which
|
|
70
|
+
# lives with the subject, cannot be observed.
|
|
186
71
|
#
|
|
187
|
-
# @
|
|
72
|
+
# @param object [Object] the object to observe
|
|
73
|
+
# @param method [Symbol, String] the method whose value may change,
|
|
74
|
+
# followed by its arguments and its block
|
|
75
|
+
# @return [Matchi::Change] a change, or one to refine with `by`,
|
|
76
|
+
# `by_at_least`, `by_at_most`, `from … to` or `to`
|
|
188
77
|
def change(object, method, ...)
|
|
189
78
|
::Matchi::Change.new(object, method, ...)
|
|
190
79
|
end
|
|
191
80
|
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
# @
|
|
195
|
-
# matcher = satisfy { |value| value == 42 }
|
|
196
|
-
# matcher.match? { 42 } # => true
|
|
197
|
-
#
|
|
198
|
-
# @yield [value] A block that defines the satisfaction criteria
|
|
199
|
-
# @yieldparam value The value to test
|
|
200
|
-
# @yieldreturn [Boolean] true if the value satisfies the criteria
|
|
201
|
-
#
|
|
202
|
-
# @return [#match?] A satisfy matcher.
|
|
203
|
-
#
|
|
204
|
-
# @api public
|
|
81
|
+
# @yieldparam value [Object] the actual value
|
|
82
|
+
# @yieldreturn [Boolean] whether it satisfies the block
|
|
83
|
+
# @return [Matchi::Satisfy]
|
|
205
84
|
def satisfy(&)
|
|
206
85
|
::Matchi::Satisfy.new(&)
|
|
207
86
|
end
|
|
208
87
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
# @
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def method_missing(name, ...)
|
|
218
|
-
return super unless predicate_matcher_name?(name)
|
|
219
|
-
|
|
220
|
-
::Matchi::Predicate.new(name, ...)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# :nocov:
|
|
224
|
-
|
|
225
|
-
# Hook method to return whether the obj can respond to id method or not.
|
|
226
|
-
def respond_to_missing?(name, include_private = false)
|
|
227
|
-
predicate_matcher_name?(name) || super
|
|
228
|
-
end
|
|
88
|
+
# @param name [Symbol, nil] the name of a registered specification
|
|
89
|
+
# @yield the document of an anonymous specification, instead of a name
|
|
90
|
+
# @return [Fix::Spec] the specification, tested against the actual value
|
|
91
|
+
# as a matcher
|
|
92
|
+
# @raise [KeyError] when no specification has that name
|
|
93
|
+
# @raise [ArgumentError] with both a name and a block, or neither
|
|
94
|
+
def fix(name = nil, &block)
|
|
95
|
+
raise ::ArgumentError, "a name or a block must be given, not both" if name.nil? == block.nil?
|
|
229
96
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# Predicate matcher name detector.
|
|
233
|
-
#
|
|
234
|
-
# @param name [Array, Symbol] The name of a potential predicate matcher.
|
|
235
|
-
#
|
|
236
|
-
# @return [Boolean] Indicates if it is a predicate matcher name or not.
|
|
237
|
-
def predicate_matcher_name?(name)
|
|
238
|
-
name.start_with?("be_", "have_") && !name.end_with?("!", "?")
|
|
97
|
+
name.nil? ? ::Fix.spec(&block) : ::Fix[name]
|
|
239
98
|
end
|
|
240
99
|
end
|
|
241
100
|
end
|
data/lib/fix/node.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fix
|
|
4
|
+
# A context of a specification, with everything needed to run its
|
|
5
|
+
# expectations: the lets in force, the chain of methods to send to the
|
|
6
|
+
# subject, and the expectations written in it.
|
|
7
|
+
#
|
|
8
|
+
# @!attribute [r] lets
|
|
9
|
+
# @return [Hash{Symbol => Proc}] the lets, frozen
|
|
10
|
+
# @!attribute [r] challenges
|
|
11
|
+
# @return [Array<Defi::Challenge>] the chain of the `on` above, frozen
|
|
12
|
+
# @!attribute [r] chain
|
|
13
|
+
# @return [String] its text, as `.name(args)` each, empty at the root
|
|
14
|
+
# @!attribute [r] expectations
|
|
15
|
+
# @return [Array<Fix::Expectation>] in the order written
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
Node = ::Data.define(:lets, :challenges, :chain, :expectations)
|
|
19
|
+
|
|
20
|
+
# An expectation: a requirement at a line of the specification.
|
|
21
|
+
#
|
|
22
|
+
# @!attribute [r] location
|
|
23
|
+
# @return [String] `file:line`
|
|
24
|
+
# @!attribute [r] requirement
|
|
25
|
+
# @return [Spectus::Requirement]
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
Expectation = ::Data.define(:location, :requirement)
|
|
29
|
+
|
|
30
|
+
private_constant :Node
|
|
31
|
+
private_constant :Expectation
|
|
32
|
+
end
|