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