mock-suey 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +342 -4
- data/lib/.rbnext/3.0/mock_suey/core.rb +246 -0
- data/lib/.rbnext/3.0/mock_suey/ext/instance_class.rb +22 -0
- data/lib/.rbnext/3.0/mock_suey/ext/rspec.rb +37 -0
- data/lib/.rbnext/3.0/mock_suey/mock_contract.rb +150 -0
- data/lib/.rbnext/3.0/mock_suey/rspec/mock_context.rb +129 -0
- data/lib/.rbnext/3.0/mock_suey/type_checks/ruby.rb +251 -0
- data/lib/.rbnext/3.1/mock_suey/core.rb +246 -0
- data/lib/.rbnext/3.1/mock_suey/mock_contract.rb +150 -0
- data/lib/.rbnext/3.1/mock_suey/rspec/mock_context.rb +129 -0
- data/lib/.rbnext/3.1/mock_suey/rspec/proxy_method_invoked.rb +47 -0
- data/lib/.rbnext/3.1/mock_suey/tracer.rb +173 -0
- data/lib/.rbnext/3.1/mock_suey/type_checks/ruby.rb +251 -0
- data/lib/mock_suey/core.rb +246 -0
- data/lib/mock_suey/ext/instance_class.rb +22 -0
- data/lib/mock_suey/ext/rspec.rb +37 -0
- data/lib/mock_suey/logging.rb +29 -0
- data/lib/mock_suey/method_call.rb +71 -0
- data/lib/mock_suey/mock_contract.rb +150 -0
- data/lib/mock_suey/rspec/mock_context.rb +129 -0
- data/lib/mock_suey/rspec/proxy_method_invoked.rb +47 -0
- data/lib/mock_suey/rspec.rb +60 -0
- data/lib/mock_suey/tracer.rb +173 -0
- data/lib/mock_suey/type_checks/ruby.rb +251 -0
- data/lib/mock_suey/type_checks.rb +8 -0
- data/lib/mock_suey/version.rb +1 -1
- data/lib/mock_suey.rb +15 -0
- metadata +27 -3
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MockSuey
|
4
|
+
module Ext
|
5
|
+
module RSpec
|
6
|
+
# Provide unified interface to access target class
|
7
|
+
# for different double/proxy types
|
8
|
+
refine ::RSpec::Mocks::TestDoubleProxy do
|
9
|
+
def target_class = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
refine ::RSpec::Mocks::PartialDoubleProxy do
|
13
|
+
def target_class = object.class
|
14
|
+
end
|
15
|
+
|
16
|
+
refine ::RSpec::Mocks::VerifyingPartialDoubleProxy do
|
17
|
+
def target_class = object.class
|
18
|
+
end
|
19
|
+
|
20
|
+
refine ::RSpec::Mocks::PartialClassDoubleProxy do
|
21
|
+
def target_class = object.singleton_class
|
22
|
+
end
|
23
|
+
|
24
|
+
refine ::RSpec::Mocks::VerifyingPartialClassDoubleProxy do
|
25
|
+
def target_class = object.singleton_class
|
26
|
+
end
|
27
|
+
|
28
|
+
refine ::RSpec::Mocks::VerifyingProxy do
|
29
|
+
def target_class = @doubled_module.target
|
30
|
+
end
|
31
|
+
|
32
|
+
refine ::RSpec::Mocks::Proxy do
|
33
|
+
attr_reader :method_doubles
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copied from Test Prof
|
4
|
+
module MockSuey
|
5
|
+
# Helper for output printing
|
6
|
+
module Logging
|
7
|
+
COLORS = {
|
8
|
+
INFO: "\e[34m", # blue
|
9
|
+
WARN: "\e[33m", # yellow
|
10
|
+
ERROR: "\e[31m" # red
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
class Formatter
|
14
|
+
def call(severity, _time, progname, msg)
|
15
|
+
colorize(severity.to_sym, "[MOCK SUEY #{severity}] #{msg}") + "\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def colorize(level, msg)
|
21
|
+
return msg unless MockSuey.config.color?
|
22
|
+
|
23
|
+
return msg unless COLORS.key?(level)
|
24
|
+
|
25
|
+
"#{COLORS[level]}#{msg}\e[0m"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mock_suey/ext/instance_class"
|
4
|
+
|
5
|
+
module MockSuey
|
6
|
+
using Ext::InstanceClass
|
7
|
+
|
8
|
+
class MethodCall < Struct.new(
|
9
|
+
:receiver_class,
|
10
|
+
:method_name,
|
11
|
+
:arguments,
|
12
|
+
:return_value,
|
13
|
+
:has_kwargs,
|
14
|
+
:metadata,
|
15
|
+
keyword_init: true
|
16
|
+
)
|
17
|
+
def initialize(**)
|
18
|
+
super
|
19
|
+
self.metadata = {} unless metadata
|
20
|
+
end
|
21
|
+
|
22
|
+
def pos_args
|
23
|
+
return arguments unless has_kwargs
|
24
|
+
*positional, _kwarg = arguments
|
25
|
+
positional
|
26
|
+
end
|
27
|
+
|
28
|
+
def kwargs
|
29
|
+
return {} unless has_kwargs
|
30
|
+
arguments.last
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_kwargs
|
34
|
+
super.then do |val|
|
35
|
+
# Flag hasn't been set explicitly,
|
36
|
+
# so we need to derive it from the method data
|
37
|
+
return val unless val.nil?
|
38
|
+
|
39
|
+
kwarg_params = keyword_parameters
|
40
|
+
return self.has_kwargs = false if kwarg_params.empty?
|
41
|
+
|
42
|
+
last_arg = arguments.last
|
43
|
+
|
44
|
+
unless last_arg.is_a?(::Hash) && last_arg.keys.all? { ::Symbol === _1 }
|
45
|
+
return self.has_kwargs = false
|
46
|
+
end
|
47
|
+
|
48
|
+
self.has_kwargs = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_desc
|
53
|
+
delimeter = receiver_class.singleton_class? ? "." : "#"
|
54
|
+
|
55
|
+
"#{receiver_class.instance_class_name}#{delimeter}#{method_name}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect
|
59
|
+
"#{method_desc}(#{arguments.map(&:inspect).join(", ")}) -> #{return_value.inspect}"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def keyword_parameters
|
65
|
+
arg_types = receiver_class.instance_method(method_name).parameters
|
66
|
+
return [] if arg_types.any? { _1[0] == :nokey }
|
67
|
+
|
68
|
+
arg_types.select { _1[0].start_with?("key") }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mock_suey/ext/instance_class"
|
4
|
+
|
5
|
+
module MockSuey
|
6
|
+
class MockContract
|
7
|
+
using Ext::InstanceClass
|
8
|
+
|
9
|
+
class Error < StandardError
|
10
|
+
attr_reader :contract
|
11
|
+
|
12
|
+
def initialize(contract, msg)
|
13
|
+
@contract = contract
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def captured_calls_message(calls)
|
20
|
+
calls.map do |call|
|
21
|
+
contract.args_pattern.map.with_index do |arg, i|
|
22
|
+
(ANYTHING == arg) ? "_" : call.arguments[i].inspect
|
23
|
+
end.join(", ").then do |args_desc|
|
24
|
+
" (#{args_desc}) -> #{call.return_value.class}"
|
25
|
+
end
|
26
|
+
end.uniq.join("\n")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NoMethodCalls < Error
|
31
|
+
def initialize(contract)
|
32
|
+
super(
|
33
|
+
contract,
|
34
|
+
"Mock contract verification failed:\n" \
|
35
|
+
" No method calls captured for #{contract.method_desc}"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class NoMatchingMethodCalls < Error
|
41
|
+
def initialize(contract, real_calls)
|
42
|
+
@contract = contract
|
43
|
+
super(
|
44
|
+
contract,
|
45
|
+
"Mock contract verification failed:\n" \
|
46
|
+
" No matching calls captured for #{contract.inspect}.\n" \
|
47
|
+
" Captured call patterns:\n" \
|
48
|
+
"#{captured_calls_message(real_calls)}"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class NoMatchingReturnType < Error
|
54
|
+
def initialize(contract, real_calls)
|
55
|
+
@contract = contract
|
56
|
+
super(
|
57
|
+
contract,
|
58
|
+
"Mock contract verification failed:\n" \
|
59
|
+
" No calls with the expected return type captured for #{contract.inspect}.\n" \
|
60
|
+
" Captured call patterns:\n" \
|
61
|
+
"#{captured_calls_message(real_calls)}"
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
ANYTHING = Object.new.freeze
|
67
|
+
|
68
|
+
def self.from_stub(call_obj)
|
69
|
+
call_obj => {receiver_class:, method_name:, return_value:}
|
70
|
+
|
71
|
+
args_pattern = call_obj.arguments.map do
|
72
|
+
contractable_arg?(_1) ? _1 : ANYTHING
|
73
|
+
end
|
74
|
+
|
75
|
+
new(
|
76
|
+
receiver_class:,
|
77
|
+
method_name:,
|
78
|
+
args_pattern:,
|
79
|
+
return_type: return_value.class
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.contractable_arg?(val)
|
84
|
+
case val
|
85
|
+
when TrueClass, FalseClass, Numeric, String, Regexp, NilClass
|
86
|
+
true
|
87
|
+
when Array
|
88
|
+
val.all? { |v| contractable_arg?(v) }
|
89
|
+
when Hash
|
90
|
+
contractable_arg?(val.values)
|
91
|
+
else
|
92
|
+
false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
attr_reader :receiver_class, :method_name,
|
97
|
+
:args_pattern, :return_type
|
98
|
+
|
99
|
+
def initialize(receiver_class:, method_name:, args_pattern:, return_type:)
|
100
|
+
@receiver_class = receiver_class
|
101
|
+
@method_name = method_name
|
102
|
+
@args_pattern = args_pattern
|
103
|
+
@return_type = return_type
|
104
|
+
end
|
105
|
+
|
106
|
+
def verify!(calls)
|
107
|
+
return if noop?
|
108
|
+
|
109
|
+
raise NoMethodCalls.new(self) if calls.nil? || calls.empty?
|
110
|
+
|
111
|
+
matching_input_calls = calls.select { matching_args?(_1) }
|
112
|
+
raise NoMatchingMethodCalls.new(self, calls) if matching_input_calls.empty?
|
113
|
+
|
114
|
+
matching_input_calls.each do
|
115
|
+
return if _1.return_value.class <= return_type
|
116
|
+
end
|
117
|
+
|
118
|
+
raise NoMatchingReturnType.new(self, matching_input_calls)
|
119
|
+
end
|
120
|
+
|
121
|
+
def noop? = args_pattern.all? { _1 == ANYTHING }
|
122
|
+
|
123
|
+
def method_desc
|
124
|
+
delimeter = receiver_class.singleton_class? ? "." : "#"
|
125
|
+
|
126
|
+
"#{receiver_class.instance_class_name}#{delimeter}#{method_name}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def inspect
|
130
|
+
args_pattern.map do
|
131
|
+
(_1 == ANYTHING) ? "_" : _1.inspect
|
132
|
+
end.join(", ").then do |args_desc|
|
133
|
+
"#{method_desc}: (#{args_desc}) -> #{return_type}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def matching_args?(call)
|
140
|
+
args_pattern.each.with_index do |arg, i|
|
141
|
+
next if arg == ANYTHING
|
142
|
+
# Use case-eq here to make it possible to use composed
|
143
|
+
# matchers in the future
|
144
|
+
return false unless arg === call.arguments[i]
|
145
|
+
end
|
146
|
+
|
147
|
+
true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mock_suey/ext/rspec"
|
4
|
+
require "mock_suey/ext/instance_class"
|
5
|
+
|
6
|
+
module MockSuey
|
7
|
+
module RSpec
|
8
|
+
# Special type of shared context for mocks.
|
9
|
+
# The main difference is that it can track mocked classes and methods.
|
10
|
+
module MockContext
|
11
|
+
NAMESPACE = "mock::"
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_writer :context_namespace
|
15
|
+
|
16
|
+
def context_namespace = @context_namespace || NAMESPACE
|
17
|
+
|
18
|
+
def collector = @collector ||= MocksCollector.new
|
19
|
+
|
20
|
+
def registry = collector.registry
|
21
|
+
end
|
22
|
+
|
23
|
+
class MocksCollector
|
24
|
+
using Ext::RSpec
|
25
|
+
using Ext::InstanceClass
|
26
|
+
|
27
|
+
# Registry contains all identified mocks/stubs in a form:
|
28
|
+
# Hash[Class: Hash[Symbol method_name, Array[MethodCall]]
|
29
|
+
attr_reader :registry
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@registry = Hash.new { |h, k| h[k] = {} }
|
33
|
+
@mocks = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def watch(context_id)
|
37
|
+
return if mocks.key?(context_id)
|
38
|
+
|
39
|
+
mocks[context_id] = true
|
40
|
+
|
41
|
+
evaluate_context!(context_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :mocks
|
47
|
+
|
48
|
+
def evaluate_context!(context_id)
|
49
|
+
store = registry
|
50
|
+
|
51
|
+
Class.new(::RSpec::Core::ExampleGroup) do
|
52
|
+
def self.metadata = {}
|
53
|
+
|
54
|
+
def self.filtered_examples = examples
|
55
|
+
|
56
|
+
::RSpec::Core::MemoizedHelpers.define_helpers_on(self)
|
57
|
+
|
58
|
+
include_context(context_id)
|
59
|
+
|
60
|
+
specify("true") { expect(true).to be(true) }
|
61
|
+
|
62
|
+
after do
|
63
|
+
::RSpec::Mocks.space.proxies.values.each do |proxy|
|
64
|
+
proxy.method_doubles.values.each do |double|
|
65
|
+
method_name = double.method_name
|
66
|
+
receiver_class = proxy.target_class
|
67
|
+
|
68
|
+
# Simple doubles don't have targets
|
69
|
+
next unless receiver_class
|
70
|
+
|
71
|
+
# TODO: Make conversion customizable (see proxy_method_invoked)
|
72
|
+
if method_name == :new && receiver_class.singleton_class?
|
73
|
+
receiver_class, method_name = receiver_class.instance_class, :initialize
|
74
|
+
end
|
75
|
+
|
76
|
+
expected_calls = store[receiver_class][method_name] = []
|
77
|
+
|
78
|
+
double.stubs.each do |stub|
|
79
|
+
arguments =
|
80
|
+
if stub.expected_args in [::RSpec::Mocks::ArgumentMatchers::NoArgsMatcher]
|
81
|
+
[]
|
82
|
+
else
|
83
|
+
stub.expected_args
|
84
|
+
end
|
85
|
+
|
86
|
+
return_value = stub.implementation.terminal_action.call
|
87
|
+
|
88
|
+
expected_calls << MethodCall.new(
|
89
|
+
receiver_class:,
|
90
|
+
method_name:,
|
91
|
+
arguments:,
|
92
|
+
return_value:
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end.run
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
module DSL
|
103
|
+
def mock_context(name, **opts, &block)
|
104
|
+
::RSpec.shared_context("#{MockContext.context_namespace}#{name}", **opts, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module ExampleGroup
|
109
|
+
def include_mock_context(name)
|
110
|
+
context_id = "#{MockContext.context_namespace}#{name}"
|
111
|
+
|
112
|
+
MockContext.collector.watch(context_id)
|
113
|
+
include_context(context_id)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Extending RSpec
|
121
|
+
RSpec.extend(MockSuey::RSpec::MockContext::DSL)
|
122
|
+
|
123
|
+
if RSpec.configuration.expose_dsl_globally?
|
124
|
+
Object.include(MockSuey::RSpec::MockContext::DSL)
|
125
|
+
Module.extend(MockSuey::RSpec::MockContext::DSL)
|
126
|
+
end
|
127
|
+
|
128
|
+
RSpec::Core::ExampleGroup.extend(MockSuey::RSpec::MockContext::ExampleGroup)
|
129
|
+
RSpec::Core::ExampleGroup.extend(MockSuey::RSpec::MockContext::DSL)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
RSpec::Mocks::VerifyingMethodDouble
|
5
|
+
rescue LoadError
|
6
|
+
warn "Couldn't find VerifyingMethodDouble class from rspec-mocks"
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
require "mock_suey/ext/rspec"
|
11
|
+
require "mock_suey/ext/instance_class"
|
12
|
+
|
13
|
+
module MockSuey
|
14
|
+
module RSpec
|
15
|
+
using Ext::RSpec
|
16
|
+
using Ext::InstanceClass
|
17
|
+
|
18
|
+
module ProxyMethodInvokedHook
|
19
|
+
def proxy_method_invoked(obj, *args, &block)
|
20
|
+
return super if obj.is_a?(::RSpec::Mocks::TestDouble) && !obj.is_a?(::RSpec::Mocks::VerifyingDouble)
|
21
|
+
|
22
|
+
receiver_class = @proxy.target_class
|
23
|
+
method_name = @method_name
|
24
|
+
|
25
|
+
# TODO: Make conversion customizable to support .perform_later -> #perform
|
26
|
+
# and other similar use-cases
|
27
|
+
if method_name == :new && receiver_class.singleton_class?
|
28
|
+
receiver_class, method_name = receiver_class.instance_class, :initialize
|
29
|
+
end
|
30
|
+
|
31
|
+
method_call = MockSuey::MethodCall.new(
|
32
|
+
receiver_class:,
|
33
|
+
method_name:,
|
34
|
+
arguments: args,
|
35
|
+
metadata: {example: ::RSpec.current_example}
|
36
|
+
)
|
37
|
+
|
38
|
+
super.tap do |ret|
|
39
|
+
method_call.return_value = ret
|
40
|
+
MockSuey.handle_mocked_call(method_call)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
RSpec::Mocks::MethodDouble.prepend(MockSuey::RSpec::ProxyMethodInvokedHook)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module MockSuey
|
6
|
+
module RSpec
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def register_example_failure(example, err)
|
10
|
+
example.execution_result.status = :failed
|
11
|
+
example.execution_result.exception = err
|
12
|
+
example.set_aggregate_failures_exception(err)
|
13
|
+
end
|
14
|
+
|
15
|
+
def report_non_example_failure(err, location = nil)
|
16
|
+
err.set_backtrace([location]) if err.backtrace.nil? && location
|
17
|
+
|
18
|
+
::RSpec.configuration.reporter.notify_non_example_exception(
|
19
|
+
err,
|
20
|
+
"An error occurred after suite run."
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "mock_suey/rspec/proxy_method_invoked"
|
27
|
+
require "mock_suey/rspec/mock_context"
|
28
|
+
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.before(:suite) do
|
31
|
+
MockSuey.cook
|
32
|
+
end
|
33
|
+
|
34
|
+
config.after(:suite) do
|
35
|
+
leftovers = MockSuey.eat
|
36
|
+
|
37
|
+
next if leftovers.empty?
|
38
|
+
|
39
|
+
failed_examples = Set.new
|
40
|
+
|
41
|
+
leftovers.each do |call_obj|
|
42
|
+
err = call_obj.metadata[:error]
|
43
|
+
example = call_obj.metadata[:example]
|
44
|
+
|
45
|
+
if example
|
46
|
+
failed_examples << example
|
47
|
+
MockSuey::RSpec.register_example_failure(example, err)
|
48
|
+
else
|
49
|
+
location = call_obj.metadata[:location]
|
50
|
+
MockSuey::RSpec.report_non_example_failure(err, location)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
failed_examples.each do
|
55
|
+
::RSpec.configuration.reporter.example_failed(_1)
|
56
|
+
end
|
57
|
+
|
58
|
+
exit(RSpec.configuration.failure_exit_code)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MockSuey
|
4
|
+
class Tracer
|
5
|
+
class PrependCollector
|
6
|
+
attr_reader :tracer
|
7
|
+
|
8
|
+
def initialize(tracer)
|
9
|
+
@tracer = tracer
|
10
|
+
end
|
11
|
+
|
12
|
+
def module_for(klass, methods)
|
13
|
+
tracer = self.tracer
|
14
|
+
|
15
|
+
Module.new do
|
16
|
+
define_method(:__mock_suey_tracer__) { tracer }
|
17
|
+
|
18
|
+
methods.each do |mid|
|
19
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
20
|
+
def #{mid}(*args, **kwargs, &block)
|
21
|
+
super.tap do |return_value|
|
22
|
+
arguments = args
|
23
|
+
arguments << kwargs unless kwargs.empty?
|
24
|
+
|
25
|
+
__mock_suey_tracer__ << MethodCall.new(
|
26
|
+
receiver_class: self.class,
|
27
|
+
method_name: __method__,
|
28
|
+
arguments: arguments,
|
29
|
+
has_kwargs: !kwargs.empty?,
|
30
|
+
return_value: return_value,
|
31
|
+
metadata: {
|
32
|
+
location: method(__method__).super_method.source_location.first
|
33
|
+
}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup(targets)
|
43
|
+
targets.each do |klass, methods|
|
44
|
+
mod = module_for(klass, methods)
|
45
|
+
klass.prepend(mod)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class TracePointCollector
|
54
|
+
attr_reader :tracer
|
55
|
+
|
56
|
+
def initialize(tracer)
|
57
|
+
@tracer = tracer
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup(targets)
|
61
|
+
tracer = self.tracer
|
62
|
+
calls_stack = []
|
63
|
+
|
64
|
+
@tp = TracePoint.trace(:call, :return) do |tp|
|
65
|
+
methods = targets[tp.defined_class]
|
66
|
+
next unless methods
|
67
|
+
next unless methods.include?(tp.method_id)
|
68
|
+
|
69
|
+
receiver_class, method_name = tp.defined_class, tp.method_id
|
70
|
+
|
71
|
+
if tp.event == :call
|
72
|
+
method = tp.self.method(method_name)
|
73
|
+
arguments = []
|
74
|
+
kwargs = {}
|
75
|
+
|
76
|
+
method.parameters.each do |(type, name)|
|
77
|
+
next if name == :** || name == :* || name == :&
|
78
|
+
|
79
|
+
val = tp.binding.local_variable_get(name)
|
80
|
+
|
81
|
+
case type
|
82
|
+
when :req, :opt
|
83
|
+
arguments << val
|
84
|
+
when :keyreq, :key
|
85
|
+
kwargs[name] = val
|
86
|
+
when :rest
|
87
|
+
arguments.concat(val)
|
88
|
+
when :keyrest
|
89
|
+
kwargs.merge!(val)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
arguments << kwargs unless kwargs.empty?
|
94
|
+
|
95
|
+
call_obj = MethodCall.new(
|
96
|
+
receiver_class:,
|
97
|
+
method_name:,
|
98
|
+
arguments:,
|
99
|
+
has_kwargs: !kwargs.empty?,
|
100
|
+
metadata: {
|
101
|
+
location: method.source_location.first
|
102
|
+
}
|
103
|
+
)
|
104
|
+
tracer << call_obj
|
105
|
+
calls_stack << call_obj
|
106
|
+
elsif tp.event == :return
|
107
|
+
call_obj = calls_stack.pop
|
108
|
+
call_obj.return_value = tp.return_value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def stop
|
114
|
+
tp.disable
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
attr_reader :tp
|
120
|
+
end
|
121
|
+
|
122
|
+
attr_reader :store, :targets, :collector
|
123
|
+
|
124
|
+
def initialize(via: :prepend)
|
125
|
+
@store = []
|
126
|
+
@targets = Hash.new { |h, k| h[k] = [] }
|
127
|
+
@collector =
|
128
|
+
if via == :prepend
|
129
|
+
PrependCollector.new(self)
|
130
|
+
elsif via == :trace_point
|
131
|
+
TracePointCollector.new(self)
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Unknown tracing method: #{via}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def collect(klass, methods)
|
138
|
+
targets[klass].concat(methods)
|
139
|
+
targets[klass].uniq!
|
140
|
+
end
|
141
|
+
|
142
|
+
def start!
|
143
|
+
collector.setup(targets)
|
144
|
+
end
|
145
|
+
|
146
|
+
def stop
|
147
|
+
collector.stop
|
148
|
+
total = store.size
|
149
|
+
filter_calls!
|
150
|
+
MockSuey.logger.debug "Collected #{store.size} real calls (#{total - store.size} were filtered)"
|
151
|
+
store
|
152
|
+
end
|
153
|
+
|
154
|
+
def <<(call_obj)
|
155
|
+
store << call_obj
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def filter_calls!
|
161
|
+
store.reject! { mocked?(_1) }
|
162
|
+
end
|
163
|
+
|
164
|
+
def mocked?(call_obj)
|
165
|
+
location = call_obj.metadata[:location]
|
166
|
+
|
167
|
+
location.match?(%r{/lib/rspec/mocks/}) ||
|
168
|
+
call_obj.return_value.is_a?(::RSpec::Mocks::Double) ||
|
169
|
+
call_obj.arguments.any? { _1.is_a?(::RSpec::Mocks::Double) } ||
|
170
|
+
call_obj.kwargs.values.any? { _1.is_a?(::RSpec::Mocks::Double) }
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|