rbs 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ module RBS
2
+ module Test
3
+ module Observer
4
+ @@observers = {}
5
+
6
+ class <<self
7
+ def notify(key, *args)
8
+ @@observers[key]&.call(*args)
9
+ end
10
+
11
+ def register(key, object = nil, &block)
12
+ @@observers[key] = object || block
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -10,30 +10,25 @@ begin
10
10
  opts = Shellwords.shellsplit(ENV["RBS_TEST_OPT"] || "-I sig")
11
11
  filter = ENV.fetch("RBS_TEST_TARGET").split(",")
12
12
  skips = (ENV["RBS_TEST_SKIP"] || "").split(",")
13
- logger.level = (ENV["RBS_TEST_LOGLEVEL"] || "info")
14
- raise_on_error = ENV["RBS_TEST_RAISE"]
13
+ sampling = !ENV.key?("RBS_TEST_NO_SAMPLE")
14
+ RBS.logger_level = (ENV["RBS_TEST_LOGLEVEL"] || "info")
15
15
  rescue
16
16
  STDERR.puts "rbs/test/setup handles the following environment variables:"
17
17
  STDERR.puts " [REQUIRED] RBS_TEST_TARGET: test target class name, `Foo::Bar,Foo::Baz` for each class or `Foo::*` for all classes under `Foo`"
18
18
  STDERR.puts " [OPTIONAL] RBS_TEST_SKIP: skip testing classes"
19
19
  STDERR.puts " [OPTIONAL] RBS_TEST_OPT: options for signatures (`-r` for libraries or `-I` for signatures)"
20
20
  STDERR.puts " [OPTIONAL] RBS_TEST_LOGLEVEL: one of debug|info|warn|error|fatal (defaults to info)"
21
- STDERR.puts " [OPTIONAL] RBS_TEST_RAISE: specify any value to raise an exception when type error is detected"
21
+ STDERR.puts " [OPTIONAL] RBS_TEST_NO_SAMPLE: if set, the type checker tests all the values of a collection"
22
22
  exit 1
23
23
  end
24
24
 
25
- hooks = []
26
-
27
- env = RBS::Environment.new
28
-
29
25
  loader = RBS::EnvironmentLoader.new
30
26
  OptionParser.new do |opts|
31
27
  opts.on("-r [LIB]") do |name| loader.add(library: name) end
32
28
  opts.on("-I [DIR]") do |dir| loader.add(path: Pathname(dir)) end
33
29
  end.parse!(opts)
34
- loader.load(env: env)
35
30
 
36
- env = env.resolve_type_names
31
+ env = RBS::Environment.from_loader(loader).resolve_type_names
37
32
 
38
33
  def match(filter, name)
39
34
  if filter.end_with?("*")
@@ -43,16 +38,18 @@ def match(filter, name)
43
38
  end
44
39
  end
45
40
 
41
+ factory = RBS::Factory.new()
42
+ tester = RBS::Test::Tester.new(env: env)
43
+
46
44
  TracePoint.trace :end do |tp|
47
- class_name = tp.self.name
45
+ class_name = tp.self.name&.yield_self {|name| factory.type_name(name).absolute! }
48
46
 
49
47
  if class_name
50
- if filter.any? {|f| match(f, class_name) } && skips.none? {|f| match(f, class_name) }
51
- type_name = RBS::Namespace.parse(class_name).absolute!.to_type_name
52
- if hooks.none? {|hook| hook.klass == tp.self }
53
- if env.class_decls.key?(type_name)
48
+ if filter.any? {|f| match(f, class_name.to_s) } && skips.none? {|f| match(f, class_name.to_s) }
49
+ if tester.checkers.none? {|hook| hook.klass == tp.self }
50
+ if env.class_decls.key?(class_name)
54
51
  logger.info "Setting up hooks for #{class_name}"
55
- hooks << RBS::Test::Hook.install(env, tp.self, logger: logger).verify_all.raise_on_error!(raise_on_error)
52
+ tester.install!(tp.self, sampling: sampling)
56
53
  end
57
54
  end
58
55
  end
@@ -1,325 +1,4 @@
1
1
  module RBS
2
2
  module Test
3
- module Spy
4
- def self.singleton_method(object, method_name)
5
- spy = SingletonSpy.new(object: object, method_name: method_name)
6
-
7
- if block_given?
8
- begin
9
- spy.setup
10
- yield spy
11
- ensure
12
- spy.reset
13
- end
14
- else
15
- spy
16
- end
17
- end
18
-
19
- def self.instance_method(mod, method_name)
20
- spy = InstanceSpy.new(mod: mod, method_name: method_name)
21
-
22
- if block_given?
23
- begin
24
- spy.setup
25
- yield spy
26
- ensure
27
- spy.reset
28
- end
29
- else
30
- spy
31
- end
32
- end
33
-
34
- def self.wrap(object, method_name)
35
- spy = WrapSpy.new(object: object, method_name: method_name)
36
-
37
- if block_given?
38
- begin
39
- yield spy, spy.wrapped_object
40
- end
41
- else
42
- spy
43
- end
44
- end
45
-
46
- class SingletonSpy
47
- attr_accessor :callback
48
- attr_reader :method_name
49
- attr_reader :object
50
-
51
- def initialize(object:, method_name:)
52
- @object = object
53
- @method_name = method_name
54
- @callback = -> (_) { }
55
- end
56
-
57
- def setup
58
- spy = self
59
-
60
- object.singleton_class.class_eval do
61
- remove_method spy.method_name
62
- define_method spy.method_name, spy.spy()
63
- end
64
- end
65
-
66
- def spy()
67
- spy = self
68
-
69
- -> (*args, &block) do
70
- return_value = nil
71
- exception = nil
72
- block_calls = []
73
-
74
- spy_block = if block
75
- Object.new.instance_eval do |fresh|
76
- -> (*block_args) do
77
- block_exn = nil
78
- block_return = nil
79
-
80
- begin
81
- block_return = if self.equal?(fresh)
82
- # no instance eval
83
- block.call(*block_args)
84
- else
85
- self.instance_exec(*block_args, &block)
86
- end
87
- rescue Exception => exn
88
- block_exn = exn
89
- end
90
-
91
- block_calls << ArgumentsReturn.new(
92
- arguments: block_args,
93
- return_value: block_return,
94
- exception: block_exn
95
- )
96
-
97
- if block_exn
98
- raise block_exn
99
- else
100
- block_return
101
- end
102
- end.ruby2_keywords
103
- end
104
- end
105
-
106
- begin
107
- return_value = super(*args, &spy_block)
108
- rescue Exception => exn
109
- exception = exn
110
- end
111
-
112
- trace = CallTrace.new(
113
- method_name: spy.method_name,
114
- method_call: ArgumentsReturn.new(
115
- arguments: args,
116
- return_value: return_value,
117
- exception: exception,
118
- ),
119
- block_calls: block_calls,
120
- block_given: block != nil
121
- )
122
-
123
- spy.callback.call(trace)
124
-
125
- if exception
126
- raise exception
127
- else
128
- return_value
129
- end
130
- end.ruby2_keywords
131
- end
132
-
133
- def reset
134
- if object.singleton_class.methods.include?(method_name)
135
- object.singleton_class.remove_method method_name
136
- end
137
- end
138
- end
139
-
140
- class InstanceSpy
141
- attr_accessor :callback
142
- attr_reader :mod
143
- attr_reader :method_name
144
- attr_reader :original_method
145
-
146
- def initialize(mod:, method_name:)
147
- @mod = mod
148
- @method_name = method_name
149
- @original_method = mod.instance_method(method_name)
150
- @callback = -> (_) { }
151
- end
152
-
153
- def setup
154
- spy = self
155
-
156
- mod.class_eval do
157
- remove_method spy.method_name
158
- define_method spy.method_name, spy.spy()
159
- end
160
- end
161
-
162
- def reset
163
- spy = self
164
-
165
- mod.class_eval do
166
- remove_method spy.method_name
167
- define_method spy.method_name, spy.original_method
168
- end
169
- end
170
-
171
- def spy
172
- spy = self
173
-
174
- -> (*args, &block) do
175
- return_value = nil
176
- exception = nil
177
- block_calls = []
178
-
179
- spy_block = if block
180
- Object.new.instance_eval do |fresh|
181
- -> (*block_args) do
182
- block_exn = nil
183
- block_return = nil
184
-
185
- begin
186
- block_return = if self.equal?(fresh)
187
- # no instance eval
188
- block.call(*block_args)
189
- else
190
- self.instance_exec(*block_args, &block)
191
- end
192
- rescue Exception => exn
193
- block_exn = exn
194
- end
195
-
196
- block_calls << ArgumentsReturn.new(
197
- arguments: block_args,
198
- return_value: block_return,
199
- exception: block_exn
200
- )
201
-
202
- if block_exn
203
- raise block_exn
204
- else
205
- block_return
206
- end
207
- end.ruby2_keywords
208
- end
209
- end
210
-
211
- begin
212
- return_value = spy.original_method.bind_call(self, *args, &spy_block)
213
- rescue Exception => exn
214
- exception = exn
215
- end
216
-
217
- trace = CallTrace.new(
218
- method_name: spy.method_name,
219
- method_call: ArgumentsReturn.new(
220
- arguments: args,
221
- return_value: return_value,
222
- exception: exception,
223
- ),
224
- block_calls: block_calls,
225
- block_given: block != nil
226
- )
227
-
228
- spy.callback.call(trace)
229
-
230
- if exception
231
- raise exception
232
- else
233
- return_value
234
- end
235
- end.ruby2_keywords
236
- end
237
- end
238
-
239
- class WrapSpy
240
- attr_accessor :callback
241
- attr_reader :object
242
- attr_reader :method_name
243
-
244
- def initialize(object:, method_name:)
245
- @callback = -> (_) { }
246
- @object = object
247
- @method_name = method_name
248
- end
249
-
250
- def wrapped_object
251
- spy = self
252
-
253
- Class.new(BasicObject) do
254
- define_method(:method_missing) do |name, *args, &block|
255
- spy.object.__send__(name, *args, &block)
256
- end
257
-
258
- define_method(spy.method_name, -> (*args, &block) {
259
- return_value = nil
260
- exception = nil
261
- block_calls = []
262
-
263
- spy_block = if block
264
- Object.new.instance_eval do |fresh|
265
- -> (*block_args) do
266
- block_exn = nil
267
- block_return = nil
268
-
269
- begin
270
- block_return = if self.equal?(fresh)
271
- # no instance eval
272
- block.call(*block_args)
273
- else
274
- self.instance_exec(*block_args, &block)
275
- end
276
- rescue Exception => exn
277
- block_exn = exn
278
- end
279
-
280
- block_calls << ArgumentsReturn.new(
281
- arguments: block_args,
282
- return_value: block_return,
283
- exception: block_exn
284
- )
285
-
286
- if block_exn
287
- raise block_exn
288
- else
289
- block_return
290
- end
291
- end.ruby2_keywords
292
- end
293
- end
294
-
295
- begin
296
- return_value = spy.object.__send__(spy.method_name, *args, &spy_block)
297
- rescue ::Exception => exn
298
- exception = exn
299
- end
300
-
301
- trace = CallTrace.new(
302
- method_name: spy.method_name,
303
- method_call: ArgumentsReturn.new(
304
- arguments: args,
305
- return_value: return_value,
306
- exception: exception,
307
- ),
308
- block_calls: block_calls,
309
- block_given: block != nil
310
- )
311
-
312
- spy.callback.call(trace)
313
-
314
- if exception
315
- spy.object.__send__(:raise, exception)
316
- else
317
- return_value
318
- end
319
- }.ruby2_keywords)
320
- end.new()
321
- end
322
- end
323
- end
324
3
  end
325
4
  end
@@ -0,0 +1,116 @@
1
+ module RBS
2
+ module Test
3
+ class Tester
4
+ attr_reader :env
5
+ attr_reader :checkers
6
+
7
+ def initialize(env:)
8
+ @env = env
9
+ @checkers = []
10
+ end
11
+
12
+ def factory
13
+ @factory ||= Factory.new
14
+ end
15
+
16
+ def builder
17
+ @builder ||= DefinitionBuilder.new(env: env)
18
+ end
19
+
20
+ def install!(klass, sampling:)
21
+ RBS.logger.info { "Installing runtime type checker in #{klass}..." }
22
+
23
+ type_name = factory.type_name(klass.name).absolute!
24
+
25
+ builder.build_instance(type_name).tap do |definition|
26
+ instance_key = new_key(type_name, "InstanceChecker")
27
+ Observer.register(instance_key, MethodCallTester.new(klass, builder, definition, kind: :instance, sampling: sampling))
28
+
29
+ definition.methods.each do |name, method|
30
+ if method.implemented_in == type_name
31
+ RBS.logger.info { "Setting up method hook in ##{name}..." }
32
+ Hook.hook_instance_method klass, name, key: instance_key
33
+ end
34
+ end
35
+ end
36
+
37
+ builder.build_singleton(type_name).tap do |definition|
38
+ singleton_key = new_key(type_name, "SingletonChecker")
39
+ Observer.register(singleton_key, MethodCallTester.new(klass.singleton_class, builder, definition, kind: :singleton, sampling: sampling))
40
+
41
+ definition.methods.each do |name, method|
42
+ if method.implemented_in == type_name || name == :new
43
+ RBS.logger.info { "Setting up method hook in .#{name}..." }
44
+ Hook.hook_singleton_method klass, name, key: singleton_key
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def new_key(type_name, prefix)
51
+ "#{prefix}__#{type_name}__#{SecureRandom.hex(10)}"
52
+ end
53
+
54
+ class TypeError < Exception
55
+ attr_reader :errors
56
+
57
+ def initialize(errors)
58
+ @errors = errors
59
+
60
+ super "TypeError: #{errors.map {|e| Errors.to_string(e) }.join(", ")}"
61
+ end
62
+ end
63
+
64
+ class MethodCallTester
65
+ attr_reader :self_class
66
+ attr_reader :definition
67
+ attr_reader :builder
68
+ attr_reader :kind
69
+ attr_reader :sampling
70
+
71
+ def initialize(self_class, builder, definition, kind:, sampling:)
72
+ @self_class = self_class
73
+ @definition = definition
74
+ @builder = builder
75
+ @kind = kind
76
+ @sampling = sampling
77
+ end
78
+
79
+ def env
80
+ builder.env
81
+ end
82
+
83
+ def check
84
+ @check ||= TypeCheck.new(self_class: self_class, builder: builder, sampling: sampling)
85
+ end
86
+
87
+ def format_method_name(name)
88
+ case kind
89
+ when :instance
90
+ "##{name}"
91
+ when :singleton
92
+ ".#{name}"
93
+ end
94
+ end
95
+
96
+ def call(receiver, trace)
97
+ method_name = trace.method_name
98
+ method = definition.methods[method_name]
99
+ if method
100
+ RBS.logger.debug { "Type checking `#{self_class}#{format_method_name(method_name)}`..."}
101
+ errors = check.overloaded_call(method, format_method_name(method_name), trace, errors: [])
102
+
103
+ if errors.empty?
104
+ RBS.logger.debug { "No type error detected 👏" }
105
+ else
106
+ RBS.logger.debug { "Detected type error 🚨" }
107
+ raise TypeError.new(errors)
108
+ end
109
+ else
110
+ RBS.logger.error { "Type checking `#{self_class}#{method_name}` call but no method found in definition" }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end