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.
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MockSuey
4
+ module TypeChecks
5
+ class MissingSignature < StandardError
6
+ end
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MockSuey # :nodoc:
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
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.1
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-15 00:00:00.000000000 Z
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: