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,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: receiver_class,
|
97
|
+
method_name: method_name,
|
98
|
+
arguments: 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
|
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "rbs", "~> 2.0"
|
4
|
+
require "rbs"
|
5
|
+
require "rbs/test"
|
6
|
+
|
7
|
+
require "set"
|
8
|
+
require "pathname"
|
9
|
+
|
10
|
+
require "mock_suey/ext/instance_class"
|
11
|
+
|
12
|
+
module MockSuey
|
13
|
+
module TypeChecks
|
14
|
+
using Ext::InstanceClass
|
15
|
+
|
16
|
+
class Ruby
|
17
|
+
class SignatureGenerator
|
18
|
+
attr_reader :klass, :method_calls, :constants, :singleton
|
19
|
+
alias_method :singleton?, :singleton
|
20
|
+
|
21
|
+
def initialize(klass, method_calls)
|
22
|
+
@klass = klass
|
23
|
+
@singleton = klass.singleton_class?
|
24
|
+
@method_calls = method_calls
|
25
|
+
@constants = Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_rbs
|
29
|
+
[
|
30
|
+
header,
|
31
|
+
*method_calls.map { |name, calls| method_sig(name, calls) },
|
32
|
+
footer
|
33
|
+
].join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def header
|
39
|
+
nesting_parts = klass.instance_class_name.split("::")
|
40
|
+
|
41
|
+
base = Kernel
|
42
|
+
nesting = 0
|
43
|
+
|
44
|
+
lines = []
|
45
|
+
|
46
|
+
nesting_parts.map do |const|
|
47
|
+
base = base.const_get(const)
|
48
|
+
lines << "#{" " * nesting}#{base.is_a?(Class) ? "class" : "module"} #{const}"
|
49
|
+
nesting += 1
|
50
|
+
end
|
51
|
+
|
52
|
+
@nesting = nesting_parts.size
|
53
|
+
|
54
|
+
lines.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def footer
|
58
|
+
@nesting.times.map do |n|
|
59
|
+
"#{" " * (@nesting - n - 1)}end"
|
60
|
+
end.join("\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
def method_sig(name, calls)
|
64
|
+
"#{" " * @nesting}def #{singleton? ? "self." : ""}#{name}: (#{[args_sig(calls.map(&:pos_args)), kwargs_sig(calls.map(&:kwargs))].compact.join(", ")}) -> (#{return_sig(name, calls.map(&:return_value))})"
|
65
|
+
end
|
66
|
+
|
67
|
+
def args_sig(args)
|
68
|
+
return if args.all?(&:empty?)
|
69
|
+
|
70
|
+
args.transpose.map do |arg_values|
|
71
|
+
arg_values.map(&:class).uniq.map do
|
72
|
+
constants << _1
|
73
|
+
"::#{_1.name}"
|
74
|
+
end
|
75
|
+
end.join(", ")
|
76
|
+
end
|
77
|
+
|
78
|
+
def kwargs_sig(kwargs)
|
79
|
+
return if kwargs.all?(&:empty?)
|
80
|
+
|
81
|
+
key_values = kwargs.each_with_object(Hash.new { |h, k| h[k] = [] }) { |pairs, acc|
|
82
|
+
pairs.each { acc[_1] << _2 }
|
83
|
+
}
|
84
|
+
|
85
|
+
key_values.map do |key, values|
|
86
|
+
values_sig = values.map(&:class).uniq.map do
|
87
|
+
constants << _1
|
88
|
+
"::#{_1.name}"
|
89
|
+
end.join(" | ")
|
90
|
+
|
91
|
+
"?#{key}: (#{values_sig})"
|
92
|
+
end.join(", ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def return_sig(name, values)
|
96
|
+
# Special case
|
97
|
+
return "self" if name == :initialize
|
98
|
+
|
99
|
+
values.map(&:class).uniq.map do
|
100
|
+
constants << _1
|
101
|
+
"::#{_1.name}"
|
102
|
+
end.join(" | ")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def initialize(load_dirs: [])
|
107
|
+
@load_dirs = Array(load_dirs)
|
108
|
+
end
|
109
|
+
|
110
|
+
def typecheck!(call_obj, raise_on_missing: false)
|
111
|
+
method_name = call_obj.method_name
|
112
|
+
|
113
|
+
method_call = RBS::Test::ArgumentsReturn.return(
|
114
|
+
arguments: call_obj.arguments,
|
115
|
+
value: call_obj.return_value
|
116
|
+
)
|
117
|
+
|
118
|
+
call_trace = RBS::Test::CallTrace.new(
|
119
|
+
method_name: method_name,
|
120
|
+
method_call: method_call,
|
121
|
+
# TODO: blocks support
|
122
|
+
block_calls: [],
|
123
|
+
block_given: false
|
124
|
+
)
|
125
|
+
|
126
|
+
method_type = type_for(call_obj.receiver_class, method_name)
|
127
|
+
|
128
|
+
unless method_type
|
129
|
+
raise MissingSignature, "No signature found for #{call_obj.method_desc}" if raise_on_missing
|
130
|
+
return
|
131
|
+
end
|
132
|
+
|
133
|
+
self_class = call_obj.receiver_class
|
134
|
+
instance_class = call_obj.receiver_class
|
135
|
+
class_class = call_obj.receiver_class.singleton_class? ? call_obj.receiver_class : call_obj.receiver_class.singleton_class
|
136
|
+
|
137
|
+
typecheck = RBS::Test::TypeCheck.new(
|
138
|
+
self_class: self_class,
|
139
|
+
builder: builder,
|
140
|
+
sample_size: 100, # What should be the value here?
|
141
|
+
unchecked_classes: [],
|
142
|
+
instance_class: instance_class,
|
143
|
+
class_class: class_class
|
144
|
+
)
|
145
|
+
|
146
|
+
errors = []
|
147
|
+
typecheck.overloaded_call(
|
148
|
+
method_type,
|
149
|
+
"#{self_class.singleton_class? ? "." : "#"}#{method_name}",
|
150
|
+
call_trace,
|
151
|
+
errors: errors
|
152
|
+
)
|
153
|
+
|
154
|
+
reject_returned_doubles!(errors)
|
155
|
+
|
156
|
+
# TODO: Use custom error class
|
157
|
+
raise RBS::Test::Tester::TypeError.new(errors) unless errors.empty?
|
158
|
+
end
|
159
|
+
|
160
|
+
def load_signatures_from_calls(calls)
|
161
|
+
constants = Set.new
|
162
|
+
|
163
|
+
calls.group_by(&:receiver_class).each do |klass, klass_calls|
|
164
|
+
calls_per_method = klass_calls.group_by(&:method_name)
|
165
|
+
generator = SignatureGenerator.new(klass, calls_per_method)
|
166
|
+
|
167
|
+
generator.to_rbs.then do |rbs|
|
168
|
+
MockSuey.logger.debug "Generated RBS for #{klass.instance_class_name}:\n#{rbs}\n"
|
169
|
+
load_rbs(rbs)
|
170
|
+
end
|
171
|
+
|
172
|
+
constants |= generator.constants
|
173
|
+
end
|
174
|
+
|
175
|
+
constants.each do |const|
|
176
|
+
next if type_defined?(const)
|
177
|
+
|
178
|
+
SignatureGenerator.new(const, {}).to_rbs.then do |rbs|
|
179
|
+
MockSuey.logger.debug "Generated RBS for constant #{const.instance_class_name}:\n#{rbs}\n"
|
180
|
+
load_rbs(rbs)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def load_rbs(rbs)
|
188
|
+
::RBS::Parser.parse_signature(rbs).then do |declarations|
|
189
|
+
declarations.each do |decl|
|
190
|
+
env << decl
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def env
|
196
|
+
return @env if instance_variable_defined?(:@env)
|
197
|
+
|
198
|
+
loader = RBS::EnvironmentLoader.new
|
199
|
+
@load_dirs&.each { loader.add(path: Pathname(_1)) }
|
200
|
+
@env = RBS::Environment.from_loader(loader).resolve_type_names
|
201
|
+
end
|
202
|
+
|
203
|
+
def builder ; @builder ||= RBS::DefinitionBuilder.new(env: env); end
|
204
|
+
|
205
|
+
def type_for(klass, method_name)
|
206
|
+
type = type_for_class(klass.instance_class)
|
207
|
+
return unless env.class_decls[type]
|
208
|
+
|
209
|
+
decl = klass.singleton_class? ? builder.build_singleton(type) : builder.build_instance(type)
|
210
|
+
|
211
|
+
decl.methods[method_name]
|
212
|
+
end
|
213
|
+
|
214
|
+
def type_for_class(klass)
|
215
|
+
*path, name = *klass.instance_class_name.split("::").map(&:to_sym)
|
216
|
+
|
217
|
+
namespace = path.empty? ? RBS::Namespace.root : RBS::Namespace.new(absolute: true, path: path)
|
218
|
+
|
219
|
+
RBS::TypeName.new(name: name, namespace: namespace)
|
220
|
+
end
|
221
|
+
|
222
|
+
def type_defined?(klass)
|
223
|
+
!env.class_decls[type_for_class(klass.instance_class)].nil?
|
224
|
+
end
|
225
|
+
|
226
|
+
def reject_returned_doubles!(errors)
|
227
|
+
return unless defined?(::RSpec::Core)
|
228
|
+
|
229
|
+
errors.reject! do |error|
|
230
|
+
case error
|
231
|
+
in RBS::Test::Errors::ReturnTypeError[
|
232
|
+
type:,
|
233
|
+
value: ::RSpec::Mocks::InstanceVerifyingDouble => double
|
234
|
+
]
|
235
|
+
return_class = type.instance_of?(RBS::Types::Bases::Self) ? error.klass : type.name
|
236
|
+
return_type = return_class.to_s.gsub(/^::/, "")
|
237
|
+
double_type = double.instance_variable_get(:@doubled_module).target.to_s
|
238
|
+
|
239
|
+
double_type == return_type
|
240
|
+
in RBS::Test::Errors::ReturnTypeError[
|
241
|
+
value: ::RSpec::Mocks::Double
|
242
|
+
]
|
243
|
+
true
|
244
|
+
else
|
245
|
+
false
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MockSuey
|
4
|
+
class Configuration
|
5
|
+
# No freezing this const to allow third-party libraries
|
6
|
+
# to integrate with mock_suey
|
7
|
+
TYPE_CHECKERS = %w[ruby]
|
8
|
+
|
9
|
+
attr_accessor :debug,
|
10
|
+
:logger,
|
11
|
+
:log_level,
|
12
|
+
:color,
|
13
|
+
:store_mocked_calls,
|
14
|
+
:signature_load_dirs,
|
15
|
+
:raise_on_missing_types,
|
16
|
+
:raise_on_missing_auto_types,
|
17
|
+
:trace_real_calls,
|
18
|
+
:trace_real_calls_via
|
19
|
+
|
20
|
+
attr_reader :type_check, :auto_type_check, :verify_mock_contracts
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@debug = %w[1 y yes true t].include?(ENV["MOCK_SUEY_DEBUG"])
|
24
|
+
@log_level = debug ? :debug : :info
|
25
|
+
@color = nil
|
26
|
+
@store_mocked_calls = false
|
27
|
+
@type_check = nil
|
28
|
+
@signature_load_dirs = ["sig"]
|
29
|
+
@raise_on_missing_types = false
|
30
|
+
@raise_on_missing_auto_types = true
|
31
|
+
@trace_real_calls = false
|
32
|
+
@auto_type_check = false
|
33
|
+
@trace_real_calls_via = :prepend
|
34
|
+
end
|
35
|
+
|
36
|
+
def color?
|
37
|
+
return color unless color.nil?
|
38
|
+
|
39
|
+
logdev = logger.instance_variable_get(:@logdev)
|
40
|
+
return self.color = false unless logdev
|
41
|
+
|
42
|
+
output = logdev.instance_variable_get(:@dev)
|
43
|
+
return self.color = false unless output
|
44
|
+
|
45
|
+
self.color = output.is_a?(IO) && output.tty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def type_check=(val)
|
49
|
+
if val.nil?
|
50
|
+
@type_check = nil
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
val = val.to_s
|
55
|
+
raise ArgumentError, "Unsupported type checker: #{val}. Supported: #{TYPE_CHECKERS.join(",")}" unless TYPE_CHECKERS.include?(val)
|
56
|
+
|
57
|
+
@type_check = val
|
58
|
+
end
|
59
|
+
|
60
|
+
def auto_type_check=(val)
|
61
|
+
if val
|
62
|
+
@trace_real_calls = true
|
63
|
+
@store_mocked_calls = true
|
64
|
+
@auto_type_check = true
|
65
|
+
else
|
66
|
+
@auto_type_check = val
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def verify_mock_contracts=(val)
|
71
|
+
if val
|
72
|
+
@trace_real_calls = true
|
73
|
+
@verify_mock_contracts = true
|
74
|
+
else
|
75
|
+
@verify_mock_contracts = val
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class << self
|
81
|
+
attr_reader :stored_mocked_calls, :tracer, :stored_real_calls
|
82
|
+
attr_accessor :type_checker
|
83
|
+
|
84
|
+
def config = @config ||= Configuration.new
|
85
|
+
|
86
|
+
def configure = yield config
|
87
|
+
|
88
|
+
def logger = config.logger
|
89
|
+
|
90
|
+
def on_mocked_call(&block)
|
91
|
+
on_mocked_callbacks << block
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_mocked_call(call_obj)
|
95
|
+
on_mocked_callbacks.each { _1.call(call_obj) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def on_mocked_callbacks
|
99
|
+
@on_mocked_callbacks ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
# Load extensions and start tracing if required
|
103
|
+
def cook
|
104
|
+
setup_logger
|
105
|
+
setup_type_checker
|
106
|
+
setup_mocked_calls_collection if config.store_mocked_calls
|
107
|
+
setup_real_calls_collection if config.trace_real_calls
|
108
|
+
end
|
109
|
+
|
110
|
+
# Run post-suite checks
|
111
|
+
def eat
|
112
|
+
@stored_real_calls = tracer.stop if config.trace_real_calls
|
113
|
+
|
114
|
+
offenses = []
|
115
|
+
|
116
|
+
if config.store_mocked_calls
|
117
|
+
logger.debug { "Stored mocked calls:\n#{stored_mocked_calls.map { " #{_1.inspect}" }.join("\n")}" }
|
118
|
+
end
|
119
|
+
|
120
|
+
if config.trace_real_calls
|
121
|
+
logger.debug { "Traced real calls:\n#{stored_real_calls.map { " #{_1.inspect}" }.join("\n")}" }
|
122
|
+
end
|
123
|
+
|
124
|
+
if config.auto_type_check
|
125
|
+
perform_auto_type_check(offenses)
|
126
|
+
end
|
127
|
+
|
128
|
+
if config.verify_mock_contracts
|
129
|
+
perform_contracts_verification(offenses)
|
130
|
+
end
|
131
|
+
|
132
|
+
offenses
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def setup_logger
|
138
|
+
if !config.logger || config.debug
|
139
|
+
config.logger = Logger.new($stdout)
|
140
|
+
config.logger.formatter = Logging::Formatter.new
|
141
|
+
end
|
142
|
+
config.logger.level = config.log_level
|
143
|
+
end
|
144
|
+
|
145
|
+
def setup_type_checker
|
146
|
+
return unless config.type_check
|
147
|
+
|
148
|
+
# Allow configuring type checher manually
|
149
|
+
unless type_checker
|
150
|
+
require "mock_suey/type_checks/#{config.type_check}"
|
151
|
+
const_name = config.type_check.split("_").map(&:capitalize).join
|
152
|
+
|
153
|
+
self.type_checker = MockSuey::TypeChecks.const_get(const_name)
|
154
|
+
.new(load_dirs: config.signature_load_dirs)
|
155
|
+
|
156
|
+
logger.info "Set up type checker: #{type_checker.class.name} (load_dirs: #{config.signature_load_dirs})"
|
157
|
+
end
|
158
|
+
|
159
|
+
raise_on_missing = config.raise_on_missing_types
|
160
|
+
|
161
|
+
on_mocked_call do |call_obj|
|
162
|
+
type_checker.typecheck!(call_obj, raise_on_missing:)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def setup_mocked_calls_collection
|
167
|
+
logger.info "Collect mocked calls (MockSuey.stored_mocked_calls)"
|
168
|
+
|
169
|
+
@stored_mocked_calls = []
|
170
|
+
|
171
|
+
on_mocked_call { @stored_mocked_calls << _1 }
|
172
|
+
end
|
173
|
+
|
174
|
+
def setup_real_calls_collection
|
175
|
+
logger.info "Collect real calls via #{config.trace_real_calls_via} (MockSuey.stored_real_calls)"
|
176
|
+
|
177
|
+
@tracer = Tracer.new(via: config.trace_real_calls_via)
|
178
|
+
|
179
|
+
MockSuey::RSpec::MockContext.registry.each do |klass, methods|
|
180
|
+
logger.debug { "Trace #{klass} methods: #{methods.keys.join(", ")}" }
|
181
|
+
tracer.collect(klass, methods.keys)
|
182
|
+
end
|
183
|
+
|
184
|
+
tracer.start!
|
185
|
+
end
|
186
|
+
|
187
|
+
def perform_auto_type_check(offenses)
|
188
|
+
raise "No type checker configured" unless type_checker
|
189
|
+
|
190
|
+
# Generate signatures
|
191
|
+
type_checker.load_signatures_from_calls(stored_real_calls)
|
192
|
+
|
193
|
+
logger.info "Type-checking mocked calls against auto-generated signatures..."
|
194
|
+
|
195
|
+
was_offenses = offenses.size
|
196
|
+
|
197
|
+
# Verify stored mocked calls
|
198
|
+
raise_on_missing = config.raise_on_missing_auto_types
|
199
|
+
|
200
|
+
stored_mocked_calls.each do |call_obj|
|
201
|
+
type_checker.typecheck!(call_obj, raise_on_missing:)
|
202
|
+
rescue RBS::Test::Tester::TypeError, TypeChecks::MissingSignature => err
|
203
|
+
call_obj.metadata[:error] = err
|
204
|
+
offenses << call_obj
|
205
|
+
end
|
206
|
+
|
207
|
+
failed_count = offenses.size - was_offenses
|
208
|
+
failed = failed_count > 0
|
209
|
+
|
210
|
+
if failed
|
211
|
+
logger.error "❌ Type-checking completed. Failed examples: #{failed_count}"
|
212
|
+
else
|
213
|
+
logger.info "✅ Type-checking completed. All good"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def perform_contracts_verification(offenses)
|
218
|
+
logger.info "Verifying mock contracts..."
|
219
|
+
real_calls_per_class_method = stored_real_calls.group_by(&:receiver_class).tap do |grouped|
|
220
|
+
grouped.transform_values! { _1.group_by(&:method_name) }
|
221
|
+
end
|
222
|
+
|
223
|
+
was_offenses = offenses.size
|
224
|
+
|
225
|
+
MockSuey::RSpec::MockContext.registry.each do |klass, methods|
|
226
|
+
methods.values.flatten.each do |stub_call|
|
227
|
+
contract = MockContract.from_stub(stub_call)
|
228
|
+
logger.debug { "Generated contract:\n #{contract.inspect}\n (from stub: #{stub_call.inspect})" }
|
229
|
+
contract.verify!(real_calls_per_class_method.dig(klass, stub_call.method_name))
|
230
|
+
rescue MockContract::Error => err
|
231
|
+
stub_call.metadata[:error] = err
|
232
|
+
offenses << stub_call
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
failed_count = offenses.size - was_offenses
|
237
|
+
failed = failed_count > 0
|
238
|
+
|
239
|
+
if failed
|
240
|
+
logger.error "❌ Verifying mock contracts completed. Failed contracts: #{failed_count}"
|
241
|
+
else
|
242
|
+
logger.info "✅ Verifying mock contracts completed. All good"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MockSuey
|
4
|
+
module Ext
|
5
|
+
module InstanceClass
|
6
|
+
refine Class do
|
7
|
+
def instance_class = self
|
8
|
+
|
9
|
+
def instance_class_name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
refine Class.singleton_class do
|
13
|
+
def instance_class
|
14
|
+
# TODO: replace with const_get
|
15
|
+
eval(instance_class_name) # rubocop:disable Security/Eval
|
16
|
+
end
|
17
|
+
|
18
|
+
def instance_class_name = inspect.sub(%r{^#<Class:}, "").sub(/>$/, "")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|