rbs 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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