fix 1.0.0.beta12 → 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 +183 -210
- 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 +92 -41
- 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,47 +3,98 @@
|
|
|
3
3
|
require "matchi"
|
|
4
4
|
|
|
5
5
|
module Fix
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# Basic Comparison:
|
|
11
|
-
# - eq(expected) # Checks equality using eql?
|
|
12
|
-
# - eql(expected) # Alias for eq
|
|
13
|
-
# - be(expected) # Checks exact object identity using equal?
|
|
14
|
-
# - equal(expected) # Alias for be
|
|
15
|
-
#
|
|
16
|
-
# Type Checking:
|
|
17
|
-
# - be_an_instance_of(class) # Checks exact class match
|
|
18
|
-
# - be_a_kind_of(class) # Checks class inheritance and module inclusion
|
|
19
|
-
#
|
|
20
|
-
# State & Changes:
|
|
21
|
-
# - change(object, method) # Base for checking state changes
|
|
22
|
-
# .by(n) # Exact change by n
|
|
23
|
-
# .by_at_least(n) # Minimum change by n
|
|
24
|
-
# .by_at_most(n) # Maximum change by n
|
|
25
|
-
# .from(old).to(new) # Change from old to new value
|
|
26
|
-
# .to(new) # Change to new value
|
|
27
|
-
#
|
|
28
|
-
# Value Testing:
|
|
29
|
-
# - be_within(delta).of(value) # Checks numeric value within delta
|
|
30
|
-
# - match(regex) # Tests against regular expression
|
|
31
|
-
# - satisfy { |value| ... } # Custom matcher with block
|
|
32
|
-
#
|
|
33
|
-
# Exceptions:
|
|
34
|
-
# - raise_exception(class) # Checks if code raises exception
|
|
35
|
-
#
|
|
36
|
-
# State Testing:
|
|
37
|
-
# - be_true # Tests for true
|
|
38
|
-
# - be_false # Tests for false
|
|
39
|
-
# - be_nil # Tests for nil
|
|
40
|
-
#
|
|
41
|
-
# Predicate Matchers:
|
|
42
|
-
# - be_* # Matches object.*? method
|
|
43
|
-
# - have_* # Matches object.has_*? method
|
|
44
|
-
#
|
|
45
|
-
# @api private
|
|
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.
|
|
46
10
|
module Matcher
|
|
47
|
-
|
|
11
|
+
# @param expected [Object] the expected value
|
|
12
|
+
# @return [Matchi::Eq] `eql?` to the expected value, of its class
|
|
13
|
+
def eq(expected)
|
|
14
|
+
::Matchi::Eq.new(expected)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param expected [Object] the expected object
|
|
18
|
+
# @return [Matchi::Be] the very same object
|
|
19
|
+
def be(expected)
|
|
20
|
+
::Matchi::Be.new(expected)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param delta [Numeric] the tolerance
|
|
24
|
+
# @return [Matchi::BeWithin] to complete with `of(expected)`
|
|
25
|
+
def be_within(delta)
|
|
26
|
+
::Matchi::BeWithin.new(delta)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param expected [Regexp] the pattern
|
|
30
|
+
# @return [Matchi::Match] a String or a Symbol matching the pattern
|
|
31
|
+
def match(expected)
|
|
32
|
+
::Matchi::Match.new(expected)
|
|
33
|
+
end
|
|
34
|
+
|
|
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)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Matchi::Be] `true` itself
|
|
43
|
+
def be_true
|
|
44
|
+
::Matchi::Be.new(true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Matchi::Be] `false` itself
|
|
48
|
+
def be_false
|
|
49
|
+
::Matchi::Be.new(false)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Matchi::Be] `nil` itself
|
|
53
|
+
def be_nil
|
|
54
|
+
::Matchi::Be.new(nil)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param expected [Class, String, Symbol] the class, or its name
|
|
58
|
+
# @return [Matchi::BeAnInstanceOf] an instance of that very class
|
|
59
|
+
def be_an_instance_of(expected)
|
|
60
|
+
::Matchi::BeAnInstanceOf.new(expected)
|
|
61
|
+
end
|
|
62
|
+
|
|
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.
|
|
71
|
+
#
|
|
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`
|
|
77
|
+
def change(object, method, ...)
|
|
78
|
+
::Matchi::Change.new(object, method, ...)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @yieldparam value [Object] the actual value
|
|
82
|
+
# @yieldreturn [Boolean] whether it satisfies the block
|
|
83
|
+
# @return [Matchi::Satisfy]
|
|
84
|
+
def satisfy(&)
|
|
85
|
+
::Matchi::Satisfy.new(&)
|
|
86
|
+
end
|
|
87
|
+
|
|
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?
|
|
96
|
+
|
|
97
|
+
name.nil? ? ::Fix.spec(&block) : ::Fix[name]
|
|
98
|
+
end
|
|
48
99
|
end
|
|
49
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
|
data/lib/fix/result.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fix
|
|
4
|
+
# The result of an expectation: where it is written, the chain of methods
|
|
5
|
+
# sent to the subject, and how it went, as Spectus says it. Plain data.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] location
|
|
8
|
+
# @return [String] `file:line`
|
|
9
|
+
# @!attribute [r] chain
|
|
10
|
+
# @return [String] the methods sent to the subject, `.name(args)` each,
|
|
11
|
+
# empty for the subject itself
|
|
12
|
+
# @!attribute [r] result
|
|
13
|
+
# @return [Spectus::Result]
|
|
14
|
+
Result = ::Data.define(:location, :chain, :result) do
|
|
15
|
+
# @return [Boolean] whether the expectation is met
|
|
16
|
+
def passed?
|
|
17
|
+
result.passed?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Symbol] `:success`, `:warning`, `:info`, `:failure` or `:error`
|
|
21
|
+
def outcome
|
|
22
|
+
result.outcome
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [String] what happened, as Spectus writes it
|
|
26
|
+
def summary
|
|
27
|
+
result.summary
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [String] one line: `file:line: Outcome: summary`, any line
|
|
31
|
+
# break of the summary written `\n`
|
|
32
|
+
def to_s
|
|
33
|
+
"#{location}: #{result}".gsub(/\R/) { "\\n" }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|