rotoscope 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/rotoscope.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ Gem::Specification.new do |s|
3
+ s.name = 'rotoscope'
4
+ s.version = '0.2.1'
5
+ s.date = '2017-06-19'
6
+
7
+ s.authors = ["Jahfer Husain"]
8
+ s.email = 'jahfer.husain@shopify.com'
9
+ s.homepage = 'https://github.com/shopify/rotoscope'
10
+ s.license = 'MIT'
11
+
12
+ s.summary = "Tracing Ruby"
13
+ s.description = "Rotoscope performs introspection of method calls in Ruby programs."
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.required_ruby_version = ">= 2.2.0"
17
+ s.extensions = %w(ext/rotoscope/extconf.rb)
18
+
19
+ s.add_development_dependency 'rake-compiler', '~> 0.9'
20
+ s.add_development_dependency 'mocha', '~> 0.14'
21
+ s.add_development_dependency 'minitest', '~> 5.0'
22
+ s.add_development_dependency 'rubocop', '~> 0.49'
23
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ class FixtureInner
3
+ def do_work
4
+ raise unless sum == 2
5
+ end
6
+
7
+ def sum
8
+ 1 + 1
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ class FixtureOuter
3
+ def initialize
4
+ @inner = FixtureInner.new
5
+ end
6
+
7
+ def do_work
8
+ @inner.do_work
9
+ end
10
+ end
data/test/monadify.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Monadify
2
+ define_method("contents") do
3
+ 42
4
+ end
5
+
6
+ def foo
7
+ false
8
+ end
9
+
10
+ def monad(value)
11
+ foo
12
+ contents
13
+ define_singleton_method("contents=") { |val| val }
14
+ self.contents = value
15
+ end
16
+ end
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift File.expand_path('../', __FILE__)
4
+ require 'rotoscope'
5
+ require 'minitest'
6
+ require 'zlib'
7
+ require 'fileutils'
8
+ require 'csv'
9
+
10
+ require 'fixture_inner'
11
+ require 'fixture_outer'
12
+ require 'monadify'
13
+
14
+ module MyModule
15
+ def module_method; end
16
+ end
17
+
18
+ module PrependedModule
19
+ def prepended_method; end
20
+ end
21
+
22
+ class Example
23
+ prepend PrependedModule
24
+ include MyModule
25
+ extend MyModule
26
+ extend Monadify
27
+
28
+ class << self
29
+ def singleton_method
30
+ true
31
+ end
32
+
33
+ def apply(val)
34
+ monad val
35
+ end
36
+ end
37
+
38
+ def normal_method
39
+ true
40
+ end
41
+
42
+ def exception_method
43
+ oops
44
+ rescue
45
+ nil
46
+ end
47
+
48
+ def yielding_method
49
+ yield
50
+ end
51
+
52
+ private
53
+
54
+ def oops
55
+ raise "I've made a terrible mistake"
56
+ end
57
+ end
58
+
59
+ ROOT_FIXTURE_PATH = File.expand_path('../', __FILE__)
60
+ INNER_FIXTURE_PATH = File.expand_path('../fixture_inner.rb', __FILE__)
61
+ OUTER_FIXTURE_PATH = File.expand_path('../fixture_outer.rb', __FILE__)
62
+ MONADIFY_PATH = File.expand_path('monadify.rb', ROOT_FIXTURE_PATH)
63
+
64
+ class RotoscopeTest < MiniTest::Test
65
+ def setup
66
+ @logfile = File.expand_path('tmp/test.csv')
67
+ end
68
+
69
+ def teardown
70
+ FileUtils.remove_file(@logfile) if File.file?(@logfile)
71
+ end
72
+
73
+ def test_new
74
+ rs = Rotoscope.new(@logfile, blacklist: ['tmp'], flatten: true)
75
+ assert rs.is_a?(Rotoscope)
76
+ end
77
+
78
+ def test_close
79
+ rs = Rotoscope.new(@logfile)
80
+ assert rs.close
81
+ end
82
+
83
+ def test_closed?
84
+ rs = Rotoscope.new(@logfile)
85
+ refute_predicate rs, :closed?
86
+ rs.close
87
+ assert_predicate rs, :closed?
88
+ end
89
+
90
+ def test_state
91
+ rs = Rotoscope.new(@logfile)
92
+ assert_equal :open, rs.state
93
+ rs.trace do
94
+ assert_equal :tracing, rs.state
95
+ end
96
+ assert_equal :open, rs.state
97
+ rs.close
98
+ assert_equal :closed, rs.state
99
+ end
100
+
101
+ def test_mark
102
+ contents = rotoscope_trace do |rs|
103
+ Example.new.normal_method
104
+ rs.mark
105
+ end
106
+
107
+ assert_includes contents.split("\n"), '--- '
108
+ end
109
+
110
+ def test_mark_with_custom_strings
111
+ mark_strings = ["Hello", "ÅÉÎØÜ åéîøü"]
112
+ contents = rotoscope_trace do |rs|
113
+ e = Example.new
114
+ e.normal_method
115
+ mark_strings.each { |str| rs.mark(str) }
116
+ end
117
+
118
+ content_lines = contents.split("\n")
119
+ mark_strings.each do |str|
120
+ assert_includes content_lines, "--- #{str}"
121
+ end
122
+ end
123
+
124
+ def test_flatten
125
+ contents = rotoscope_trace(flatten: true) do
126
+ Example.new.normal_method
127
+ end
128
+
129
+ assert_equal [
130
+ { entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
131
+ { entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "Example", caller_method_name: "new", caller_method_level: "class" },
132
+ { entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
133
+ ], parse_and_normalize(contents)
134
+ end
135
+
136
+ def test_flatten_removes_duplicates
137
+ contents = rotoscope_trace(flatten: true) do
138
+ e = Example.new
139
+ 10.times { e.normal_method }
140
+ end
141
+
142
+ assert_equal [
143
+ { entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
144
+ { entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "Example", caller_method_name: "new", caller_method_level: "class" },
145
+ { entity: "Integer", method_name: "times", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
146
+ { entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "Integer", caller_method_name: "times", caller_method_level: "instance" },
147
+ ], parse_and_normalize(contents)
148
+ end
149
+
150
+ def test_start_trace_and_stop_trace
151
+ rs = Rotoscope.new(@logfile)
152
+ rs.start_trace
153
+ Example.new.normal_method
154
+ rs.stop_trace
155
+ rs.close
156
+ contents = File.read(@logfile)
157
+
158
+ assert_equal [
159
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
160
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
161
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
162
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
163
+ { event: "call", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
164
+ { event: "return", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
165
+ ], parse_and_normalize(contents)
166
+
167
+ assert_frames_consistent contents
168
+ end
169
+
170
+ def test_traces_instance_method
171
+ contents = rotoscope_trace { Example.new.normal_method }
172
+ assert_equal [
173
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
174
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
175
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
176
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
177
+ { event: "call", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
178
+ { event: "return", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 }
179
+ ], parse_and_normalize(contents)
180
+
181
+ assert_frames_consistent contents
182
+ end
183
+
184
+ def test_traces_yielding_method
185
+ contents = rotoscope_trace do
186
+ e = Example.new
187
+ e.yielding_method { e.normal_method }
188
+ end
189
+
190
+ assert_equal [
191
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
192
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
193
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
194
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
195
+ { event: "call", entity: "Example", method_name: "yielding_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
196
+ { event: "call", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
197
+ { event: "return", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
198
+ { event: "return", entity: "Example", method_name: "yielding_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 }
199
+ ], parse_and_normalize(contents)
200
+
201
+ assert_frames_consistent contents
202
+ end
203
+
204
+ def test_calls_are_consistent_after_exception
205
+ contents = rotoscope_trace { Example.new.exception_method }
206
+ assert_frames_consistent contents
207
+ end
208
+
209
+ def test_traces_and_formats_singletons_of_a_class
210
+ contents = rotoscope_trace { Example.singleton_method }
211
+ assert_equal [
212
+ { event: "call", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
213
+ { event: "return", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 }
214
+ ], parse_and_normalize(contents)
215
+
216
+ assert_frames_consistent contents
217
+ end
218
+
219
+ def test_traces_and_formats_singletons_of_an_instance
220
+ contents = rotoscope_trace { Example.new.singleton_class.singleton_method }
221
+ assert_equal [
222
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
223
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
224
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
225
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
226
+ { event: "call", entity: "Example", method_name: "singleton_class", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
227
+ { event: "return", entity: "Example", method_name: "singleton_class", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
228
+ { event: "call", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
229
+ { event: "return", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
230
+ ], parse_and_normalize(contents)
231
+
232
+ assert_frames_consistent contents
233
+ end
234
+
235
+ def test_traces_included_module_method
236
+ contents = rotoscope_trace { Example.new.module_method }
237
+ assert_equal [
238
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
239
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
240
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
241
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
242
+ { event: "call", entity: "Example", method_name: "module_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
243
+ { event: "return", entity: "Example", method_name: "module_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 }
244
+ ], parse_and_normalize(contents)
245
+
246
+ assert_frames_consistent contents
247
+ end
248
+
249
+ def test_traces_extended_module_method
250
+ contents = rotoscope_trace { Example.module_method }
251
+ assert_equal [
252
+ { event: "call", entity: "Example", method_name: "module_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
253
+ { event: "return", entity: "Example", method_name: "module_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 }
254
+ ], parse_and_normalize(contents)
255
+
256
+ assert_frames_consistent contents
257
+ end
258
+
259
+ def test_traces_prepended_module_method
260
+ contents = rotoscope_trace { Example.new.prepended_method }
261
+ assert_equal [
262
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
263
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
264
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
265
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
266
+ { event: "call", entity: "Example", method_name: "prepended_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
267
+ { event: "return", entity: "Example", method_name: "prepended_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 }
268
+ ], parse_and_normalize(contents)
269
+
270
+ assert_frames_consistent contents
271
+ end
272
+
273
+ def test_trace_ignores_calls_if_blacklisted
274
+ contents = rotoscope_trace(blacklist: [INNER_FIXTURE_PATH, OUTER_FIXTURE_PATH]) do
275
+ foo = FixtureOuter.new
276
+ foo.do_work
277
+ end
278
+
279
+ assert_equal [
280
+ { event: "call", entity: "FixtureOuter", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
281
+ { event: "call", entity: "FixtureOuter", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
282
+ { event: "return", entity: "FixtureOuter", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
283
+ { event: "return", entity: "FixtureOuter", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
284
+ { event: "call", entity: "FixtureOuter", method_name: "do_work", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
285
+ { event: "return", entity: "FixtureOuter", method_name: "do_work", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
286
+ ], parse_and_normalize(contents)
287
+
288
+ assert_frames_consistent contents
289
+ end
290
+
291
+ def test_trace_ignores_writes_in_fork
292
+ contents = rotoscope_trace do |rotoscope|
293
+ fork do
294
+ Example.singleton_method
295
+ rotoscope.mark
296
+ rotoscope.close
297
+ end
298
+ Example.singleton_method
299
+ Process.wait
300
+ end
301
+ assert_equal [
302
+ { event: "call", entity: "RotoscopeTest", method_name: "fork", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
303
+ { event: "return", entity: "RotoscopeTest", method_name: "fork", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
304
+ { event: "call", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
305
+ { event: "return", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
306
+ { event: "call", entity: "Process", method_name: "wait", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
307
+ { event: "return", entity: "Process", method_name: "wait", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
308
+ ], parse_and_normalize(contents)
309
+ end
310
+
311
+ def test_trace_disabled_on_close
312
+ contents = rotoscope_trace do |rotoscope|
313
+ Example.singleton_method
314
+ rotoscope.close
315
+ rotoscope.mark
316
+ Example.singleton_method
317
+ end
318
+ assert_equal [
319
+ { event: "call", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
320
+ { event: "return", entity: "Example", method_name: "singleton_method", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
321
+ ], parse_and_normalize(contents)
322
+ end
323
+
324
+ def test_trace_flatten
325
+ contents = rotoscope_trace(flatten: true) { Example.new.normal_method }
326
+ assert_equal [
327
+ { entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
328
+ { entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "Example", caller_method_name: "new", caller_method_level: "class" },
329
+ { entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
330
+ ], parse_and_normalize(contents)
331
+ end
332
+
333
+ def test_trace_flatten_across_files
334
+ contents = rotoscope_trace(flatten: true) do
335
+ foo = FixtureOuter.new
336
+ foo.do_work
337
+ end
338
+
339
+ assert_equal [
340
+ { entity: "FixtureOuter", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
341
+ { entity: "FixtureOuter", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "FixtureOuter", caller_method_name: "new", caller_method_level: "class" },
342
+ { entity: "FixtureInner", method_name: "new", method_level: "class", filepath: "/fixture_outer.rb", lineno: -1, caller_entity: "FixtureOuter", caller_method_name: "initialize", caller_method_level: "instance" },
343
+ { entity: "FixtureInner", method_name: "initialize", method_level: "instance", filepath: "/fixture_outer.rb", lineno: -1, caller_entity: "FixtureInner", caller_method_name: "new", caller_method_level: "class" },
344
+ { entity: "FixtureOuter", method_name: "do_work", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
345
+ { entity: "FixtureInner", method_name: "do_work", method_level: "instance", filepath: "/fixture_outer.rb", lineno: -1, caller_entity: "FixtureOuter", caller_method_name: "do_work", caller_method_level: "instance" },
346
+ { entity: "FixtureInner", method_name: "sum", method_level: "instance", filepath: "/fixture_inner.rb", lineno: -1, caller_entity: "FixtureInner", caller_method_name: "do_work", caller_method_level: "instance" }
347
+ ], parse_and_normalize(contents)
348
+ end
349
+
350
+ def test_trace_uses_io_objects
351
+ string_io = StringIO.new
352
+ Rotoscope.trace(string_io) do
353
+ Example.new.normal_method
354
+ end
355
+ refute_predicate string_io, :closed?
356
+ assert_predicate string_io, :eof?
357
+ contents = string_io.string
358
+
359
+ assert_equal [
360
+ { event: "call", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
361
+ { event: "call", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
362
+ { event: "return", entity: "Example", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
363
+ { event: "return", entity: "Example", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
364
+ { event: "call", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
365
+ { event: "return", entity: "Example", method_name: "normal_method", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
366
+ ], parse_and_normalize(contents)
367
+
368
+ assert_frames_consistent contents
369
+ end
370
+
371
+ def test_stop_trace_before_start_does_not_raise
372
+ rs = Rotoscope.new(@logfile)
373
+ rs.stop_trace
374
+ end
375
+
376
+ def test_gc_rotoscope_without_stop_trace_does_not_crash
377
+ proc {
378
+ rs = Rotoscope.new(@logfile)
379
+ rs.start_trace
380
+ }.call
381
+ GC.start
382
+ end
383
+
384
+ def test_gc_rotoscope_without_stop_trace_does_not_break_process_cleanup
385
+ child_pid = fork do
386
+ rs = Rotoscope.new(@logfile)
387
+ rs.start_trace
388
+ end
389
+ Process.waitpid(child_pid)
390
+ assert_equal true, $CHILD_STATUS.success?
391
+ end
392
+
393
+ def test_log_path
394
+ rs = Rotoscope.new(File.expand_path('tmp/test.csv.gz'))
395
+ GC.start
396
+ assert_equal File.expand_path('tmp/test.csv.gz'), rs.log_path
397
+ end
398
+
399
+ def test_ignores_calls_inside_of_threads
400
+ thread = nil
401
+ contents = rotoscope_trace do
402
+ thread = Thread.new { Example.new }
403
+ end
404
+ thread.join
405
+
406
+ assert_equal [
407
+ { event: "call", entity: "Thread", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
408
+ { event: "call", entity: "Thread", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
409
+ { event: "return", entity: "Thread", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
410
+ { event: "return", entity: "Thread", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
411
+ ], parse_and_normalize(contents)
412
+ end
413
+
414
+ def test_dynamic_class_creation
415
+ contents = rotoscope_trace { Class.new }
416
+
417
+ assert_equal [
418
+ { event: "call", entity: "Class", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
419
+ { event: "call", entity: "#<Class:0xXXXXXX>", method_name: "initialize", method_level: "instance", filepath: "/rotoscope_test.rb", lineno: -1 },
420
+ { event: "call", entity: "Object", method_name: "inherited", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
421
+ { event: "return", entity: "Object", method_name: "inherited", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
422
+ { event: "return", entity: "#<Class:0xXXXXXX>", method_name: "initialize", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
423
+ { event: "return", entity: "Class", method_name: "new", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 }
424
+ ], parse_and_normalize(contents)
425
+ end
426
+
427
+ def test_dynamic_methods_in_blacklist
428
+ skip <<-FAILING_TEST_CASE
429
+ Return events for dynamically created methods (define_method, define_singleton_method)
430
+ do not have the correct stack frame information (the call of a dynamically defined method
431
+ is correctly treated as a Ruby :call, but its return must be treated as a :c_return)
432
+ FAILING_TEST_CASE
433
+
434
+ contents = rotoscope_trace(blacklist: [MONADIFY_PATH]) { Example.apply("my value!") }
435
+
436
+ assert_equal [
437
+ { event: "call", entity: "Example", method_name: "apply", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
438
+ { event: "call", entity: "Example", method_name: "monad", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
439
+ { event: "return", entity: "Example", method_name: "monad", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
440
+ { event: "return", entity: "Example", method_name: "apply", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1 },
441
+ ], parse_and_normalize(contents)
442
+ end
443
+
444
+ def test_flatten_with_dynamic_methods_in_blacklist
445
+ # the failing test above passes when using `flatten: true` since unmatched stack returns are ignored
446
+ contents = rotoscope_trace(blacklist: [MONADIFY_PATH], flatten: true) { Example.apply("my value!") }
447
+
448
+ assert_equal [
449
+ { entity: "Example", method_name: "apply", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "<ROOT>", caller_method_name: "<UNKNOWN>", caller_method_level: "<UNKNOWN>" },
450
+ { entity: "Example", method_name: "monad", method_level: "class", filepath: "/rotoscope_test.rb", lineno: -1, caller_entity: "Example", caller_method_name: "apply", caller_method_level: "class" },
451
+ ], parse_and_normalize(contents)
452
+ end
453
+
454
+ private
455
+
456
+ def parse_and_normalize(csv_string)
457
+ CSV.parse(csv_string, headers: true, header_converters: :symbol).map do |row|
458
+ row = row.to_h
459
+ row[:lineno] = -1
460
+ row[:filepath] = row[:filepath].gsub(ROOT_FIXTURE_PATH, '')
461
+ row[:entity] = row[:entity].gsub(/:0x[a-fA-F0-9]{4,}/m, ":0xXXXXXX")
462
+ row
463
+ end
464
+ end
465
+
466
+ def assert_frames_consistent(csv_string)
467
+ assert_equal csv_string.scan(/\Acall/).size, csv_string.scan(/\Areturn/).size
468
+ end
469
+
470
+ def rotoscope_trace(blacklist: [], flatten: false)
471
+ Rotoscope.trace(@logfile, blacklist: blacklist, flatten: flatten) { |rotoscope| yield rotoscope }
472
+ File.read(@logfile)
473
+ end
474
+
475
+ def unzip(path)
476
+ File.open(path) { |f| Zlib::GzipReader.new(f).read }
477
+ end
478
+ end
479
+
480
+ # https://github.com/seattlerb/minitest/pull/683 needed to use
481
+ # autorun without affecting the exit status of forked processes
482
+ Minitest.run(ARGV)