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,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:,
|
120
|
+
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:,
|
139
|
+
builder:,
|
140
|
+
sample_size: 100, # What should be the value here?
|
141
|
+
unchecked_classes: [],
|
142
|
+
instance_class:,
|
143
|
+
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:
|
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:)
|
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:)
|
218
|
+
|
219
|
+
RBS::TypeName.new(name:, 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
|
data/lib/mock_suey/version.rb
CHANGED
data/lib/mock_suey.rb
CHANGED
@@ -1,3 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
require "ruby-next"
|
6
|
+
|
7
|
+
require "ruby-next/language/setup"
|
8
|
+
RubyNext::Language.setup_gem_load_path(transpile: true)
|
9
|
+
|
3
10
|
require "mock_suey/version"
|
11
|
+
require "mock_suey/logging"
|
12
|
+
require "mock_suey/core"
|
13
|
+
require "mock_suey/method_call"
|
14
|
+
require "mock_suey/type_checks"
|
15
|
+
require "mock_suey/tracer"
|
16
|
+
require "mock_suey/mock_contract"
|
17
|
+
|
18
|
+
require "mock_suey/rspec" if defined?(RSpec::Core)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mock-suey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-11-
|
11
|
+
date: 2022-11-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.9'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name: ruby-next
|
56
|
+
name: ruby-next-core
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -76,8 +76,32 @@ files:
|
|
76
76
|
- CHANGELOG.md
|
77
77
|
- LICENSE.txt
|
78
78
|
- README.md
|
79
|
+
- lib/.rbnext/3.0/mock_suey/core.rb
|
80
|
+
- lib/.rbnext/3.0/mock_suey/ext/instance_class.rb
|
81
|
+
- lib/.rbnext/3.0/mock_suey/ext/rspec.rb
|
82
|
+
- lib/.rbnext/3.0/mock_suey/mock_contract.rb
|
83
|
+
- lib/.rbnext/3.0/mock_suey/rspec/mock_context.rb
|
84
|
+
- lib/.rbnext/3.0/mock_suey/type_checks/ruby.rb
|
85
|
+
- lib/.rbnext/3.1/mock_suey/core.rb
|
86
|
+
- lib/.rbnext/3.1/mock_suey/mock_contract.rb
|
87
|
+
- lib/.rbnext/3.1/mock_suey/rspec/mock_context.rb
|
88
|
+
- lib/.rbnext/3.1/mock_suey/rspec/proxy_method_invoked.rb
|
89
|
+
- lib/.rbnext/3.1/mock_suey/tracer.rb
|
90
|
+
- lib/.rbnext/3.1/mock_suey/type_checks/ruby.rb
|
79
91
|
- lib/mock-suey.rb
|
80
92
|
- lib/mock_suey.rb
|
93
|
+
- lib/mock_suey/core.rb
|
94
|
+
- lib/mock_suey/ext/instance_class.rb
|
95
|
+
- lib/mock_suey/ext/rspec.rb
|
96
|
+
- lib/mock_suey/logging.rb
|
97
|
+
- lib/mock_suey/method_call.rb
|
98
|
+
- lib/mock_suey/mock_contract.rb
|
99
|
+
- lib/mock_suey/rspec.rb
|
100
|
+
- lib/mock_suey/rspec/mock_context.rb
|
101
|
+
- lib/mock_suey/rspec/proxy_method_invoked.rb
|
102
|
+
- lib/mock_suey/tracer.rb
|
103
|
+
- lib/mock_suey/type_checks.rb
|
104
|
+
- lib/mock_suey/type_checks/ruby.rb
|
81
105
|
- lib/mock_suey/version.rb
|
82
106
|
homepage: http://github.com/test-prof/mock-suey
|
83
107
|
licenses:
|