mocktail 0.0.1
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 +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +8 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +20 -0
- data/README.md +557 -0
- data/Rakefile +11 -0
- data/bin/console +35 -0
- data/bin/setup +8 -0
- data/lib/mocktail/dsl.rb +21 -0
- data/lib/mocktail/errors.rb +15 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +16 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +21 -0
- data/lib/mocktail/handles_dry_call/logs_call.rb +7 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +57 -0
- data/lib/mocktail/handles_dry_call.rb +19 -0
- data/lib/mocktail/handles_dry_new_call.rb +36 -0
- data/lib/mocktail/imitates_type/ensures_imitation_support.rb +11 -0
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +95 -0
- data/lib/mocktail/imitates_type/makes_double.rb +18 -0
- data/lib/mocktail/imitates_type.rb +19 -0
- data/lib/mocktail/initializes_mocktail.rb +17 -0
- data/lib/mocktail/matcher_presentation.rb +15 -0
- data/lib/mocktail/matchers/any.rb +18 -0
- data/lib/mocktail/matchers/base.rb +25 -0
- data/lib/mocktail/matchers/captor.rb +52 -0
- data/lib/mocktail/matchers/includes.rb +24 -0
- data/lib/mocktail/matchers/is_a.rb +11 -0
- data/lib/mocktail/matchers/matches.rb +13 -0
- data/lib/mocktail/matchers/not.rb +11 -0
- data/lib/mocktail/matchers/numeric.rb +18 -0
- data/lib/mocktail/matchers/that.rb +24 -0
- data/lib/mocktail/matchers.rb +14 -0
- data/lib/mocktail/records_demonstration.rb +32 -0
- data/lib/mocktail/registers_matcher.rb +52 -0
- data/lib/mocktail/registers_stubbing.rb +19 -0
- data/lib/mocktail/replaces_next.rb +36 -0
- data/lib/mocktail/replaces_type/redefines_new.rb +26 -0
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +39 -0
- data/lib/mocktail/replaces_type.rb +26 -0
- data/lib/mocktail/resets_state.rb +9 -0
- data/lib/mocktail/share/determines_matching_calls.rb +60 -0
- data/lib/mocktail/share/simulates_argument_error.rb +28 -0
- data/lib/mocktail/value/cabinet.rb +41 -0
- data/lib/mocktail/value/call.rb +15 -0
- data/lib/mocktail/value/demo_config.rb +10 -0
- data/lib/mocktail/value/double.rb +11 -0
- data/lib/mocktail/value/matcher_registry.rb +19 -0
- data/lib/mocktail/value/stubbing.rb +24 -0
- data/lib/mocktail/value/top_shelf.rb +61 -0
- data/lib/mocktail/value/type_replacement.rb +11 -0
- data/lib/mocktail/value.rb +8 -0
- data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +15 -0
- data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +10 -0
- data/lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb +47 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +63 -0
- data/lib/mocktail/verifies_call.rb +29 -0
- data/lib/mocktail/version.rb +3 -0
- data/lib/mocktail.rb +63 -0
- data/mocktail.gemspec +31 -0
- metadata +107 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class RecordsDemonstration
|
3
|
+
def record(demonstration, demo_config)
|
4
|
+
cabinet = Mocktail.cabinet
|
5
|
+
prior_call_count = Mocktail.cabinet.calls.dup.size
|
6
|
+
|
7
|
+
begin
|
8
|
+
cabinet.demonstration_in_progress = true
|
9
|
+
ValidatesArguments.optional(demo_config.ignore_arity) do
|
10
|
+
demonstration.call(Mocktail.matchers)
|
11
|
+
end
|
12
|
+
ensure
|
13
|
+
cabinet.demonstration_in_progress = false
|
14
|
+
end
|
15
|
+
|
16
|
+
if prior_call_count + 1 == cabinet.calls.size
|
17
|
+
cabinet.calls.pop
|
18
|
+
elsif prior_call_count == cabinet.calls.size
|
19
|
+
raise MissingDemonstrationError.new <<~MSG.tr("\n", " ")
|
20
|
+
`stubs` & `verify` expect an invocation of a mocked method by a passed
|
21
|
+
block, but no invocation occurred.
|
22
|
+
MSG
|
23
|
+
else
|
24
|
+
raise AmbiguousDemonstrationError.new <<~MSG.tr("\n", " ")
|
25
|
+
`stubs` & `verify` expect exactly one invocation of a mocked method,
|
26
|
+
but #{cabinet.calls.size - prior_call_count} were detected. As a
|
27
|
+
result, Mocktail doesn't know which invocation to stub or verify.
|
28
|
+
MSG
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class RegistersMatcher
|
3
|
+
def register(matcher_type)
|
4
|
+
if invalid_type?(matcher_type)
|
5
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
6
|
+
Matchers must be Ruby classes
|
7
|
+
MSG
|
8
|
+
elsif invalid_name?(matcher_type)
|
9
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
10
|
+
#{matcher_type.name}.matcher_name must return a valid method name
|
11
|
+
MSG
|
12
|
+
elsif invalid_match?(matcher_type)
|
13
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
14
|
+
#{matcher_type.name}#match? must be defined as a one-argument method
|
15
|
+
MSG
|
16
|
+
elsif invalid_flag?(matcher_type)
|
17
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
18
|
+
#{matcher_type.name}#is_mocktail_matcher? must be defined
|
19
|
+
MSG
|
20
|
+
else
|
21
|
+
MatcherRegistry.instance.add(matcher_type)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def invalid_type?(matcher_type)
|
28
|
+
!matcher_type.is_a?(Class)
|
29
|
+
end
|
30
|
+
|
31
|
+
def invalid_name?(matcher_type)
|
32
|
+
return true unless matcher_type.respond_to?(:matcher_name)
|
33
|
+
name = matcher_type.matcher_name
|
34
|
+
|
35
|
+
!(name.is_a?(String) || name.is_a?(Symbol)) ||
|
36
|
+
name.to_sym.inspect.start_with?(":\"")
|
37
|
+
end
|
38
|
+
|
39
|
+
def invalid_match?(matcher_type)
|
40
|
+
params = matcher_type.instance_method(:match?).parameters
|
41
|
+
params.size > 1 || ![:req, :opt].include?(params.first[0])
|
42
|
+
rescue NameError
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def invalid_flag?(matcher_type)
|
47
|
+
!matcher_type.instance_method(:is_mocktail_matcher?)
|
48
|
+
rescue NameError
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "records_demonstration"
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class RegistersStubbing
|
5
|
+
def initialize
|
6
|
+
@records_demonstration = RecordsDemonstration.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def register(demonstration, demo_config)
|
10
|
+
Stubbing.new(
|
11
|
+
demonstration: demonstration,
|
12
|
+
demo_config: demo_config,
|
13
|
+
recording: @records_demonstration.record(demonstration, demo_config)
|
14
|
+
).tap do |stubbing|
|
15
|
+
Mocktail.cabinet.store_stubbing(stubbing)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class ReplacesNext
|
3
|
+
def initialize
|
4
|
+
@top_shelf = TopShelf.instance
|
5
|
+
@redefines_new = RedefinesNew.new
|
6
|
+
@imitates_type = ImitatesType.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def replace(type, count)
|
10
|
+
raise UnsupportedMocktail.new("Mocktail.of_next() only supports classes") unless type.is_a?(Class)
|
11
|
+
|
12
|
+
mocktails = count.times.map { @imitates_type.imitate(type) }
|
13
|
+
|
14
|
+
@top_shelf.register_of_next_replacement!(type)
|
15
|
+
@redefines_new.redefine(type)
|
16
|
+
mocktails.reverse_each do |mocktail|
|
17
|
+
Mocktail.stubs(
|
18
|
+
ignore_extra_args: true,
|
19
|
+
ignore_block: true,
|
20
|
+
ignore_arity: true,
|
21
|
+
times: 1
|
22
|
+
) {
|
23
|
+
type.new
|
24
|
+
}.with {
|
25
|
+
if mocktail == mocktails.last
|
26
|
+
@top_shelf.unregister_of_next_replacement!(type)
|
27
|
+
end
|
28
|
+
|
29
|
+
mocktail
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
mocktails.size == 1 ? mocktails.first : mocktails
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class RedefinesNew
|
3
|
+
def initialize
|
4
|
+
@handles_dry_new_call = HandlesDryNewCall.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def redefine(type)
|
8
|
+
type_replacement = TopShelf.instance.type_replacement_for(type)
|
9
|
+
|
10
|
+
if type_replacement.replacement_new.nil?
|
11
|
+
type_replacement.original_new = type.method(:new)
|
12
|
+
type.singleton_class.send(:undef_method, :new)
|
13
|
+
handles_dry_new_call = @handles_dry_new_call
|
14
|
+
type.define_singleton_method :new, ->(*args, **kwargs, &block) {
|
15
|
+
if TopShelf.instance.new_replaced?(type) ||
|
16
|
+
TopShelf.instance.of_next_registered?(type)
|
17
|
+
handles_dry_new_call.handle(type, args, kwargs, block)
|
18
|
+
else
|
19
|
+
type_replacement.original_new.call(*args, **kwargs, &block)
|
20
|
+
end
|
21
|
+
}
|
22
|
+
type_replacement.replacement_new = type.singleton_method(:new)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class RedefinesSingletonMethods
|
3
|
+
def initialize
|
4
|
+
@handles_dry_call = HandlesDryCall.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def redefine(type)
|
8
|
+
type_replacement = TopShelf.instance.type_replacement_for(type)
|
9
|
+
return unless type_replacement.replacement_methods.nil?
|
10
|
+
|
11
|
+
type_replacement.original_methods = type.singleton_methods.map { |name|
|
12
|
+
type.method(name)
|
13
|
+
} - [type_replacement.replacement_new]
|
14
|
+
|
15
|
+
handles_dry_call = @handles_dry_call
|
16
|
+
type_replacement.replacement_methods = type_replacement.original_methods.map { |original_method|
|
17
|
+
type.singleton_class.send(:undef_method, original_method.name)
|
18
|
+
type.define_singleton_method original_method.name, ->(*args, **kwargs, &block) {
|
19
|
+
if TopShelf.instance.singleton_methods_replaced?(type)
|
20
|
+
handles_dry_call.handle(Call.new(
|
21
|
+
singleton: true,
|
22
|
+
double: type,
|
23
|
+
original_type: type,
|
24
|
+
dry_type: type,
|
25
|
+
method: original_method.name,
|
26
|
+
original_method: original_method,
|
27
|
+
args: args,
|
28
|
+
kwargs: kwargs,
|
29
|
+
block: block
|
30
|
+
))
|
31
|
+
else
|
32
|
+
original_method.call(*args, **kwargs, &block)
|
33
|
+
end
|
34
|
+
}
|
35
|
+
type.singleton_method(original_method.name)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative "replaces_type/redefines_new"
|
2
|
+
require_relative "replaces_type/redefines_singleton_methods"
|
3
|
+
|
4
|
+
module Mocktail
|
5
|
+
class ReplacesType
|
6
|
+
def initialize
|
7
|
+
@top_shelf = TopShelf.instance
|
8
|
+
@redefines_new = RedefinesNew.new
|
9
|
+
@redefines_singleton_methods = RedefinesSingletonMethods.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def replace(type)
|
13
|
+
unless type.is_a?(Class) || type.is_a?(Module)
|
14
|
+
raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules")
|
15
|
+
end
|
16
|
+
|
17
|
+
if type.is_a?(Class)
|
18
|
+
@top_shelf.register_new_replacement!(type)
|
19
|
+
@redefines_new.redefine(type)
|
20
|
+
end
|
21
|
+
|
22
|
+
@top_shelf.register_singleton_method_replacement!(type)
|
23
|
+
@redefines_singleton_methods.redefine(type)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class DeterminesMatchingCalls
|
3
|
+
def determine(real_call, demo_call, demo_config)
|
4
|
+
real_call.double == demo_call.double &&
|
5
|
+
real_call.method == demo_call.method &&
|
6
|
+
|
7
|
+
# Matcher implementation will replace this:
|
8
|
+
args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
|
9
|
+
kwargs_match?(real_call.kwargs, demo_call.kwargs, demo_config.ignore_extra_args) &&
|
10
|
+
blocks_match?(real_call.block, demo_call.block, demo_config.ignore_block)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def args_match?(real_args, demo_args, ignore_extra_args)
|
16
|
+
return true if ignore_extra_args && demo_args.empty?
|
17
|
+
|
18
|
+
(
|
19
|
+
real_args.size == demo_args.size ||
|
20
|
+
(ignore_extra_args && real_args.size >= demo_args.size)
|
21
|
+
) &&
|
22
|
+
demo_args.each.with_index.all? { |demo_arg, i|
|
23
|
+
match?(real_args[i], demo_arg)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def kwargs_match?(real_kwargs, demo_kwargs, ignore_extra_args)
|
28
|
+
return true if ignore_extra_args && demo_kwargs.empty?
|
29
|
+
|
30
|
+
(
|
31
|
+
real_kwargs.size == demo_kwargs.size ||
|
32
|
+
(ignore_extra_args && real_kwargs.size >= demo_kwargs.size)
|
33
|
+
) &&
|
34
|
+
demo_kwargs.all? { |key, demo_val|
|
35
|
+
real_kwargs.key?(key) && match?(real_kwargs[key], demo_val)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def blocks_match?(real_block, demo_block, ignore_block)
|
40
|
+
ignore_block ||
|
41
|
+
(real_block.nil? && demo_block.nil?) ||
|
42
|
+
(
|
43
|
+
real_block && demo_block &&
|
44
|
+
(
|
45
|
+
demo_block == real_block ||
|
46
|
+
demo_block.call(real_block)
|
47
|
+
)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def match?(real_arg, demo_arg)
|
52
|
+
if demo_arg.respond_to?(:is_mocktail_matcher?) &&
|
53
|
+
demo_arg.is_mocktail_matcher?
|
54
|
+
demo_arg.match?(real_arg)
|
55
|
+
else
|
56
|
+
demo_arg == real_arg
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class SimulatesArgumentError
|
3
|
+
def simulate(arg_params, args, kwarg_params, kwargs)
|
4
|
+
req_args = arg_params.count { |type, _| type == :req }
|
5
|
+
opt_args = arg_params.count { |type, _| type == :opt }
|
6
|
+
rest_args = arg_params.any? { |type, _| type == :rest }
|
7
|
+
req_kwargs = kwarg_params.select { |type, _| type == :keyreq }
|
8
|
+
|
9
|
+
allowed_args = req_args + opt_args
|
10
|
+
msg = if args.size < req_args || (!rest_args && args.size > allowed_args)
|
11
|
+
expected_desc = if rest_args
|
12
|
+
"#{req_args}+"
|
13
|
+
elsif allowed_args != req_args
|
14
|
+
"#{req_args}..#{allowed_args}"
|
15
|
+
else
|
16
|
+
req_args.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
"wrong number of arguments (given #{args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.map { |_, name| name }.join(", ")}" unless req_kwargs.empty?})"
|
20
|
+
|
21
|
+
elsif !(missing_kwargs = req_kwargs.reject { |_, name| kwargs.key?(name) }).empty?
|
22
|
+
"missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |_, name| name.inspect }.join(", ")}"
|
23
|
+
end
|
24
|
+
|
25
|
+
ArgumentError.new(msg)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# The Cabinet stores all thread-local state, so anything that goes here
|
2
|
+
# is guaranteed by Mocktail to be local to the currently-running thread
|
3
|
+
module Mocktail
|
4
|
+
class Cabinet
|
5
|
+
attr_writer :demonstration_in_progress
|
6
|
+
attr_reader :calls, :stubbings
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@doubles = []
|
10
|
+
@calls = []
|
11
|
+
@stubbings = []
|
12
|
+
@demonstration_in_progress = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset!
|
16
|
+
@calls = []
|
17
|
+
@stubbings = []
|
18
|
+
# Could cause an exception or prevent pollution—you decide!
|
19
|
+
@demonstration_in_progress = false
|
20
|
+
# note we don't reset doubles as they don't carry any
|
21
|
+
# user-meaningful state on them, and clearing them on reset could result
|
22
|
+
# in valid mocks being broken and stop working
|
23
|
+
end
|
24
|
+
|
25
|
+
def store_double(double)
|
26
|
+
@doubles << double
|
27
|
+
end
|
28
|
+
|
29
|
+
def store_call(call)
|
30
|
+
@calls << call
|
31
|
+
end
|
32
|
+
|
33
|
+
def store_stubbing(stubbing)
|
34
|
+
@stubbings << stubbing
|
35
|
+
end
|
36
|
+
|
37
|
+
def demonstration_in_progress?
|
38
|
+
@demonstration_in_progress
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class MatcherRegistry
|
3
|
+
def self.instance
|
4
|
+
@matcher_registry ||= new
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@matchers = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(matcher_type)
|
12
|
+
@matchers[matcher_type.matcher_name] = matcher_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(name)
|
16
|
+
@matchers[name]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class Stubbing < Struct.new(
|
3
|
+
:demonstration,
|
4
|
+
:demo_config,
|
5
|
+
:satisfaction_count,
|
6
|
+
:recording,
|
7
|
+
:effect,
|
8
|
+
keyword_init: true
|
9
|
+
)
|
10
|
+
|
11
|
+
def initialize(**kwargs)
|
12
|
+
super
|
13
|
+
self.satisfaction_count ||= 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def satisfied!
|
17
|
+
self.satisfaction_count += 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def with(&block)
|
21
|
+
self.effect = block
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# The top shelf stores all cross-thread & thread-aware state, so anything that
|
2
|
+
# goes here is on its own when it comes to ensuring thread safety.
|
3
|
+
module Mocktail
|
4
|
+
class TopShelf
|
5
|
+
def self.instance
|
6
|
+
@self ||= new
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@type_replacements = {}
|
11
|
+
@new_registrations = {}
|
12
|
+
@of_next_registrations = {}
|
13
|
+
@singleton_method_registrations = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def type_replacement_for(type)
|
17
|
+
@type_replacements[type] ||= TypeReplacement.new(type: type)
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset_current_thread!
|
21
|
+
@new_registrations[Thread.current] = []
|
22
|
+
@of_next_registrations[Thread.current] = []
|
23
|
+
@singleton_method_registrations[Thread.current] = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def register_new_replacement!(type)
|
27
|
+
@new_registrations[Thread.current] ||= []
|
28
|
+
@new_registrations[Thread.current] |= [type]
|
29
|
+
end
|
30
|
+
|
31
|
+
def new_replaced?(type)
|
32
|
+
@new_registrations[Thread.current] ||= []
|
33
|
+
@new_registrations[Thread.current].include?(type)
|
34
|
+
end
|
35
|
+
|
36
|
+
def register_of_next_replacement!(type)
|
37
|
+
@of_next_registrations[Thread.current] ||= []
|
38
|
+
@of_next_registrations[Thread.current] |= [type]
|
39
|
+
end
|
40
|
+
|
41
|
+
def of_next_registered?(type)
|
42
|
+
@of_next_registrations[Thread.current] ||= []
|
43
|
+
@of_next_registrations[Thread.current].include?(type)
|
44
|
+
end
|
45
|
+
|
46
|
+
def unregister_of_next_replacement!(type)
|
47
|
+
@of_next_registrations[Thread.current] ||= []
|
48
|
+
@of_next_registrations[Thread.current] -= [type]
|
49
|
+
end
|
50
|
+
|
51
|
+
def register_singleton_method_replacement!(type)
|
52
|
+
@singleton_method_registrations[Thread.current] ||= []
|
53
|
+
@singleton_method_registrations[Thread.current] |= [type]
|
54
|
+
end
|
55
|
+
|
56
|
+
def singleton_methods_replaced?(type)
|
57
|
+
@singleton_method_registrations[Thread.current] ||= []
|
58
|
+
@singleton_method_registrations[Thread.current].include?(type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require_relative "value/cabinet"
|
2
|
+
require_relative "value/call"
|
3
|
+
require_relative "value/demo_config"
|
4
|
+
require_relative "value/double"
|
5
|
+
require_relative "value/matcher_registry"
|
6
|
+
require_relative "value/stubbing"
|
7
|
+
require_relative "value/top_shelf"
|
8
|
+
require_relative "value/type_replacement"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "../share/determines_matching_calls"
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class FindsVerifiableCalls
|
5
|
+
def initialize
|
6
|
+
@determines_matching_calls = DeterminesMatchingCalls.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def find(recording, demo_config)
|
10
|
+
Mocktail.cabinet.calls.select { |call|
|
11
|
+
@determines_matching_calls.determine(call, recording, demo_config)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class StringifiesCall
|
3
|
+
def stringify(call)
|
4
|
+
"#{call.method}#{args_to_s(call)}#{blockify(call.block)}"
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def args_to_s(call)
|
10
|
+
unless (args_lists = [
|
11
|
+
argify(call.args),
|
12
|
+
kwargify(call.kwargs),
|
13
|
+
lambdafy(call.block)
|
14
|
+
].compact).empty?
|
15
|
+
"(#{args_lists.join(", ")})"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def argify(args)
|
20
|
+
return unless args && !args.empty?
|
21
|
+
args.map(&:inspect).join(", ")
|
22
|
+
end
|
23
|
+
|
24
|
+
def kwargify(kwargs)
|
25
|
+
return unless kwargs && !kwargs.empty?
|
26
|
+
kwargs.map { |key, val| "#{key}: #{val.inspect}" }.join(", ")
|
27
|
+
end
|
28
|
+
|
29
|
+
def lambdafy(block)
|
30
|
+
return unless block&.lambda?
|
31
|
+
"&lambda[#{source_locationify(block)}]"
|
32
|
+
end
|
33
|
+
|
34
|
+
def blockify(block)
|
35
|
+
return unless block && !block.lambda?
|
36
|
+
" { Proc at #{source_locationify(block)} }"
|
37
|
+
end
|
38
|
+
|
39
|
+
def source_locationify(block)
|
40
|
+
"#{strip_pwd(block.source_location[0])}:#{block.source_location[1]}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def strip_pwd(path)
|
44
|
+
path.gsub(Dir.pwd + File::SEPARATOR, "")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|