chaos_detector 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/bin/detect_chaos +31 -0
  3. data/lib/chaos_detector.rb +22 -0
  4. data/lib/chaos_detector/chaos_graphs/chaos_edge.rb +32 -0
  5. data/lib/chaos_detector/chaos_graphs/chaos_graph.rb +389 -0
  6. data/lib/chaos_detector/chaos_graphs/domain_metrics.rb +19 -0
  7. data/lib/chaos_detector/chaos_graphs/domain_node.rb +57 -0
  8. data/lib/chaos_detector/chaos_graphs/function_node.rb +112 -0
  9. data/lib/chaos_detector/chaos_graphs/module_node.rb +86 -0
  10. data/lib/chaos_detector/chaos_utils.rb +57 -0
  11. data/lib/chaos_detector/graph_theory/appraiser.rb +162 -0
  12. data/lib/chaos_detector/graph_theory/edge.rb +76 -0
  13. data/lib/chaos_detector/graph_theory/graph.rb +144 -0
  14. data/lib/chaos_detector/graph_theory/loop_detector.rb +32 -0
  15. data/lib/chaos_detector/graph_theory/node.rb +70 -0
  16. data/lib/chaos_detector/graph_theory/node_metrics.rb +68 -0
  17. data/lib/chaos_detector/graph_theory/reduction.rb +40 -0
  18. data/lib/chaos_detector/graphing/directed_graphs.rb +396 -0
  19. data/lib/chaos_detector/graphing/graphs.rb +129 -0
  20. data/lib/chaos_detector/graphing/matrix_graphs.rb +101 -0
  21. data/lib/chaos_detector/navigator.rb +237 -0
  22. data/lib/chaos_detector/options.rb +51 -0
  23. data/lib/chaos_detector/stacker/comp_info.rb +42 -0
  24. data/lib/chaos_detector/stacker/fn_info.rb +44 -0
  25. data/lib/chaos_detector/stacker/frame.rb +34 -0
  26. data/lib/chaos_detector/stacker/frame_stack.rb +63 -0
  27. data/lib/chaos_detector/stacker/mod_info.rb +24 -0
  28. data/lib/chaos_detector/tracker.rb +276 -0
  29. data/lib/chaos_detector/utils/core_util.rb +117 -0
  30. data/lib/chaos_detector/utils/fs_util.rb +49 -0
  31. data/lib/chaos_detector/utils/lerp_util.rb +20 -0
  32. data/lib/chaos_detector/utils/log_util.rb +45 -0
  33. data/lib/chaos_detector/utils/str_util.rb +90 -0
  34. data/lib/chaos_detector/utils/tensor_util.rb +21 -0
  35. data/lib/chaos_detector/walkman.rb +214 -0
  36. metadata +147 -0
@@ -0,0 +1,51 @@
1
+ require 'chaos_detector/chaos_utils'
2
+ require 'chaos_detector/graph_theory/node'
3
+
4
+ module ChaosDetector
5
+ class Options
6
+ extend ChaosDetector::Utils::CoreUtil::ChaosAttr
7
+
8
+ # TODO: Ability to run on self:
9
+ IGNORE_MODULES = %w[
10
+ ChaosDetector
11
+ ChaosUtils
12
+ RSpec
13
+ FactoryBot
14
+ ].freeze
15
+
16
+ IGNORE_PATHS = [
17
+
18
+ ]
19
+
20
+ # chaos_attr (:options) { ChaosDetector::Options.new }
21
+ chaos_attr(:app_root_path, Dir.getwd)
22
+ chaos_attr(:log_root_path, 'logs')
23
+ chaos_attr(:graph_render_folder, 'render')
24
+ chaos_attr(:path_domain_hash)
25
+ chaos_attr(:ignore_modules, IGNORE_MODULES.dup)
26
+ chaos_attr(:ignore_paths, IGNORE_PATHS.dup)
27
+ chaos_attr(:module_filter, 'todo')
28
+ chaos_attr(:root_label, 'App Container')
29
+ chaos_attr(:frame_csv_path, 'csv/chaos_frames.csv')
30
+ chaos_attr(:walkman_buffer_length, 1000)
31
+
32
+ def path_with_root(key:nil, path:nil)
33
+ raise ArgumentError, "key: OR path: must be set" if key.nil? && path.nil?
34
+
35
+ subpath = key ? send(key.to_sym) : path.to_s
36
+ File.join(app_root_path, subpath)
37
+ end
38
+
39
+ def domain_from_path(local_path)
40
+ # dpath = Pathname.new(path.to_s).cleanpath.to_s
41
+ # @domain_hash[dpath] = group
42
+ #
43
+
44
+ # @domain_hash = {}
45
+ # @options.path_domain_hash && options.path_domain_hash.each do |path, group|
46
+ #
47
+ key = path_domain_hash.keys.find { |k| local_path.start_with?(k.to_s) }
48
+ key ? path_domain_hash[key] : ChaosDetector::GraphTheory::Node::ROOT_NODE_NAME
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ require 'chaos_detector/chaos_utils'
2
+
3
+ module ChaosDetector
4
+ module Stacker
5
+ # Base class for Component (Module, FN) Infos
6
+ COMPONENT_TYPES = %i[function module domain].freeze
7
+ class CompInfo
8
+ attr_accessor :path
9
+ attr_accessor :name
10
+ attr_accessor :info
11
+
12
+ def initialize(name:, path: nil, info: nil)
13
+ @name = name
14
+ @path = path
15
+ @info = info
16
+ end
17
+
18
+ def ==(other)
19
+ other &&
20
+ name == other.name &&
21
+ path == other.path &&
22
+ info == other.info
23
+ end
24
+
25
+ def eql?(other)
26
+ self == other
27
+ end
28
+
29
+ def hash
30
+ [path, name, info].hash
31
+ end
32
+
33
+ def to_s
34
+ "#{name}: #{path} - #{info}"
35
+ end
36
+
37
+ def component_type
38
+ raise NotImplementedError, 'Deriving class should implement #component_type'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ require 'chaos_detector/chaos_utils'
2
+ require_relative 'comp_info'
3
+ module ChaosDetector
4
+ module Stacker
5
+ class FnInfo < ChaosDetector::Stacker::CompInfo
6
+ alias fn_name name
7
+ alias fn_line info
8
+ alias fn_path path
9
+
10
+ def initialize(fn_name:, fn_line: nil, fn_path: nil)
11
+ super(name: fn_name, path: fn_path, info: fn_line)
12
+ end
13
+
14
+ def ==(other)
15
+ ChaosDetector::Stacker::FnInfo.match?(self, other)
16
+ end
17
+
18
+ def fn_info
19
+ self
20
+ end
21
+
22
+ def to_s
23
+ "##{fn_name}: #{fn_path}:L#{fn_line}"
24
+ end
25
+
26
+ def component_type
27
+ :function
28
+ end
29
+
30
+ class << self
31
+ def match?(obj1, obj2, line_matching: false)
32
+ obj1.fn_path == obj2.fn_path && obj1.fn_name == obj2.fn_name
33
+ # (obj1.fn_name == obj2.fn_name || line_match?(obj1.fn_line, obj2.fn_line))
34
+ end
35
+
36
+ def line_match?(l1, l2)
37
+ return false if l1.nil? || l2.nil?
38
+
39
+ (l2 - l1).between?(0, 1)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ require 'chaos_detector/chaos_utils'
2
+ require 'chaos_detector/stacker/mod_info'
3
+ require 'chaos_detector/stacker/fn_info'
4
+
5
+ # A single stack (tracepoint) frame
6
+ module ChaosDetector
7
+ module Stacker
8
+ class Frame
9
+ attr_reader :event # :call, :return, :superclass, :association, :class_association
10
+ attr_reader :mod_info
11
+ attr_reader :fn_info
12
+ attr_reader :caller_info
13
+
14
+ def initialize(event:, mod_info:, fn_info:, caller_info:)
15
+ raise ArgumentError, 'event is required' if ChaosUtils.naught?(event)
16
+ # raise ArgumentError, 'mod_info is required' if ChaosUtils.naught?(mod_info)
17
+ raise ArgumentError, 'fn_info is required' if ChaosUtils.naught?(fn_info)
18
+
19
+ @mod_info = mod_info
20
+ @fn_info = fn_info
21
+ @caller_info = caller_info
22
+ @event = event.to_sym
23
+ end
24
+
25
+ def to_s
26
+ ChaosUtils.decorate_tuple(
27
+ [event, mod_info, fn_info, caller_info],
28
+ join_str: ' ',
29
+ clamp: :bracket
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,63 @@
1
+ require_relative 'frame'
2
+
3
+ require 'chaos_detector/chaos_utils'
4
+ # Maintains all nodes and infers edges as stack calls are pushed and popped via Frames.
5
+ module ChaosDetector
6
+ module Stacker
7
+ class FrameStack
8
+ def initialize
9
+ @methods = []
10
+ @modules = []
11
+ end
12
+
13
+ def log(msg, **opts)
14
+ ChaosUtils.log_msg(msg, subject: 'FrameStack', **opts)
15
+ end
16
+
17
+ def depth
18
+ @stack.length
19
+ end
20
+
21
+ def peek
22
+ @stack.first
23
+ end
24
+
25
+ def pop(frame)
26
+ raise ArgumentError, 'Current Frame is required' if frame.nil?
27
+
28
+ popped_frame, n_frame = @stack.each_with_index.find do |f, n|
29
+ if f == frame
30
+ true
31
+ elsif n.zero? && frame.fn_name == f.fn_name
32
+ # log("Matching #{f} \nto:\n #{frame} as most recent entry in stack.")
33
+ true
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ # if n_frame.nil?
40
+ # log("Could not find #{frame} in stack")
41
+ # log(self.inspect)
42
+ # end
43
+
44
+ @stack.slice!(0..n_frame) unless n_frame.nil?
45
+ [popped_frame, n_frame]
46
+ end
47
+
48
+ def push(frame)
49
+ @stack.unshift(frame)
50
+ end
51
+
52
+ def to_s
53
+ 'Frames: %d' % depth
54
+ end
55
+
56
+ def inspect
57
+ msg = "#{self}\n"
58
+ msg << ChaosUtils.decorate_tuple(@stack.map { |f| f.to_s}, join_str: " -> \n", indent_length: 2, clamp: :none)
59
+ msg
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ require 'chaos_detector/utils/str_util'
2
+ require 'chaos_detector/chaos_utils'
3
+ require_relative 'comp_info'
4
+ module ChaosDetector
5
+ module Stacker
6
+ class ModInfo < ChaosDetector::Stacker::CompInfo
7
+ alias mod_name name
8
+ alias mod_type info
9
+ alias mod_path path
10
+
11
+ def initialize(mod_name:, mod_type: nil, mod_path: nil)
12
+ super(name: mod_name, path: mod_path, info: mod_type)
13
+ end
14
+
15
+ def component_type
16
+ :module
17
+ end
18
+
19
+ def to_s
20
+ format('(%s) %s - %s', mod_type, ChaosDetector::Utils::StrUtil.humanize_module(mod_name, sep_token: '::'), ChaosDetector::Utils::StrUtil.humanize_module(mod_path, sep_token: '/'))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,276 @@
1
+ require 'set'
2
+ require 'pathname'
3
+ require_relative 'options'
4
+ require_relative 'stacker/mod_info'
5
+ require_relative 'chaos_graphs/module_node'
6
+ require_relative 'stacker/frame'
7
+ require_relative 'walkman'
8
+ require 'chaos_detector/chaos_utils'
9
+
10
+ # The main interface for intercepting tracepoints,
11
+ # and converting them into recordable and playable
12
+ # stack/trace frames
13
+
14
+ module ChaosDetector
15
+ class Tracker
16
+ REGEX_MODULE_UNDECORATE = /#<(Class:)?([a-zA-Z\:]*)(.*)>/.freeze
17
+ TRACE_METHOD_EVENTS = %i[call return].freeze
18
+
19
+ attr_reader :options
20
+ attr_reader :walkman
21
+
22
+ def initialize(options:)
23
+ raise ArgumentError, '#initialize requires options' if options.nil?
24
+
25
+ @options = options
26
+ @total_frames = 0
27
+ @total_traces = 0
28
+ apply_options
29
+ end
30
+
31
+ def record
32
+ log("Detecting chaos at #{@app_root_path}")
33
+ # log(caller_locations.join("\n\t->\t"))
34
+ # log("")
35
+ @stopped = false
36
+ @walkman.record_start
37
+ @total_traces = 0
38
+ @trace = TracePoint.new(*TRACE_METHOD_EVENTS) do |tracepoint|
39
+ if @stopped
40
+ @trace.disable
41
+ log('Tracing stopped; stopping immediately.')
42
+ next
43
+ end
44
+
45
+ tp_path = tracepoint.path
46
+ next if full_path_skip?(tp_path)
47
+
48
+ tp_class = tracepoint.defined_class
49
+
50
+ # trace_mod_details(tracepoint)
51
+ mod_info = mod_info_at(tp_class, mod_full_path: tp_path)
52
+ # puts "mod_info: #{mod_info} #{tp_class.respond_to?(:superclass) && tp_class.superclass}"
53
+ next unless mod_info
54
+
55
+ fn_info = fn_info_at(tracepoint)
56
+ e = tracepoint.event
57
+ @trace.disable do
58
+ @total_traces += 1
59
+ caller_info = extract_caller(tracepoint, fn_info)
60
+ write_event_frame(e, fn_info: fn_info, mod_info: mod_info, caller_info: caller_info)
61
+
62
+ # Detect superclass association:
63
+ ChaosUtils.with(superclass_mod_info(tp_class)) do |super_mod_info|
64
+ # puts "Would superclass #{mod_info} with #{super_mod_info}"
65
+ write_event_frame(:superclass, fn_info: fn_info, mod_info: mod_info, caller_info: super_mod_info)
66
+ end
67
+
68
+ # Detect associations:
69
+ # puts "UGGGGG: #{tp_class.singleton_class.included_modules}"
70
+ ancestor_mod_infos(tp_class, tp_class.included_modules).each do |agg_mod_info|
71
+ # puts "Would ancestors with #{agg_mod_info}"
72
+ write_event_frame(:association, fn_info: fn_info, mod_info: mod_info, caller_info: agg_mod_info)
73
+ end
74
+
75
+ # DerivedFracker.singleton_class.included_modules # MixinCD, Kernel
76
+ ancestor_mod_infos(tp_class, tp_class.singleton_class.included_modules).each do |agg_mod_info|
77
+ # puts "WOULD CLASS ancestors with #{agg_mod_info}"
78
+ write_event_frame(:class_association, fn_info: fn_info, mod_info: mod_info, caller_info: agg_mod_info)
79
+ end
80
+
81
+ # Detect class associations:
82
+ # ancestor_mod_infos(tp_class).each do |agg_mod_info|
83
+ # puts "Would ancestors with #{agg_mod_info}"
84
+ # write_event_frame(:association, fn_info: fn_info, mod_info: mod_info, caller_info: super_mod_info)
85
+ # end
86
+ end
87
+ end
88
+ @trace.enable
89
+ true
90
+ end
91
+
92
+ def write_event_frame(event, fn_info:, mod_info:, caller_info:)
93
+ ChaosDetector::Stacker::Frame.new(
94
+ event: event,
95
+ mod_info: mod_info,
96
+ fn_info: fn_info,
97
+ caller_info: caller_info
98
+ ).tap do |frame|
99
+ @walkman.write_frame(frame)
100
+ @total_frames += 1
101
+ end
102
+ end
103
+
104
+ def stop
105
+ @stopped = true
106
+ @trace&.disable
107
+ log("Stopping after total traces: #{@total_traces}")
108
+ @walkman.stop
109
+ end
110
+
111
+ # Undecorate all this junk:
112
+ # a="#<Class:Authentication>"
113
+ # b="#<Class:Person(id: integer, first"
114
+ # c="#<ChaosDetector::Node:0x00007fdd5d2c6b08>"
115
+
116
+ # Blank class get mod_class for tracepoint. [#<Class:#<Parslet::Context:0x00007fa90ee06c80>>]
117
+ # MMMM >>> (word), (default), (word), (lib/email_parser.rb):L106, (#<Parslet::Context:0x00007fa90ee06c80>)
118
+ def undecorate_module_name(mod_name)
119
+ return nil if ChaosUtils.naught?(mod_name)
120
+ return mod_name unless mod_name.start_with?('#')
121
+
122
+ plain_name = nil
123
+ caps = mod_name.match(REGEX_MODULE_UNDECORATE)&.captures
124
+ # puts "CAP #{mod_name}: #{caps}"
125
+ if caps && caps.length > 0
126
+ caps.delete('Class:')
127
+ caps.compact!
128
+ plain_name = caps.first
129
+ plain_name&.chomp!(':')
130
+ end
131
+
132
+ plain_name || mod_name
133
+ end
134
+
135
+ private
136
+
137
+ def apply_options
138
+ @walkman = ChaosDetector::Walkman.new(options: @options)
139
+ @app_root_path = ChaosUtils.with(@options.app_root_path) { |p| Pathname.new(p)&.to_s}
140
+ end
141
+
142
+ def extract_caller(tracepoint, fn_info)
143
+ callers = tracepoint.self.send(:caller_locations)
144
+ callers = callers.select do |bt|
145
+ !full_path_skip?(bt.absolute_path) &&
146
+ ChaosUtils.aught?(bt.base_label) &&
147
+ !bt.base_label.start_with?('<')
148
+ end
149
+
150
+ frame_at = callers.index { |bt| bt.base_label == fn_info.fn_name && localize_path(bt.absolute_path) == fn_info.fn_path }
151
+ bt_caller = frame_at.nil? ? nil : callers[frame_at + 1]
152
+ ChaosUtils.with(bt_caller) do |bt|
153
+ ChaosDetector::Stacker::FnInfo.new(
154
+ fn_name: bt.base_label,
155
+ fn_line: bt.lineno,
156
+ fn_path: localize_path(bt.absolute_path)
157
+ )
158
+ end
159
+ end
160
+
161
+ def superclass_mod_info(clz)
162
+ return nil unless clz&.respond_to?(:superclass)
163
+
164
+ sup_clz = clz.superclass
165
+
166
+ # puts "BOOOO::: #{clz.superclass} <> #{sup_clz} ~> ChaosUtils.aught?(sup_clz)"
167
+ return nil unless ChaosUtils.aught?(sup_clz)
168
+
169
+ # puts "DDDDDDDDDDD::: #{sup_clz&.name}"
170
+
171
+ mod_info_at(sup_clz)
172
+ end
173
+
174
+ def ancestor_mod_infos(clz, clz_modules)
175
+ sup_clz = clz.superclass rescue nil
176
+
177
+ ancestors = clz_modules.filter_map do |c|
178
+ if c != clz && (sup_clz.nil? || c != sup_clz)
179
+ mod_info_at(c)
180
+ end
181
+ end
182
+
183
+ ancestors.compact
184
+ end
185
+
186
+ def mod_info_at(mod_class, mod_full_path: nil)
187
+ return nil unless mod_class
188
+
189
+ mod_name = mod_name_from_class(mod_class)
190
+ if ChaosUtils.aught?(mod_name)
191
+ mod_type = mod_type_from_class(mod_class)
192
+ mod_fp = ChaosUtils.aught?(mod_full_path) ? mod_full_path : nil
193
+ mod_fp ||= mod_class.const_source_location(mod_name)&.first
194
+ safe_mod_info(mod_name, mod_type, mod_fp)
195
+ end
196
+ end
197
+
198
+ def fn_info_at(tracepoint)
199
+ ChaosDetector::Stacker::FnInfo.new(fn_name: tracepoint.callee_id.to_s, fn_line: tracepoint.lineno, fn_path: localize_path(tracepoint.path))
200
+ end
201
+
202
+ # TODO: MAKE more LIKE module_skip below:
203
+ def full_path_skip?(path)
204
+ return true unless ChaosUtils.aught?(path)
205
+
206
+ if !(@app_root_path && path.start_with?(@app_root_path))
207
+ true
208
+ # elsif path.start_with?('/Users/stevenmiers/src/sci-ex/sciex3/lib/mixins')
209
+ # false
210
+ else
211
+ rel_path = localize_path(path)
212
+ @options.ignore_paths.any? { |p| rel_path.start_with?(p)}
213
+ # false
214
+ end
215
+ end
216
+
217
+ def module_skip?(mod_name)
218
+ ChaosUtils.with(mod_name) do |mod|
219
+ @options.ignore_modules.any? { |m| mod.start_with?(m)}
220
+ end
221
+ end
222
+
223
+ def mod_type_from_class(clz)
224
+ case clz
225
+ when Class
226
+ :class
227
+ when Module
228
+ :module
229
+ else
230
+ log "Unknown mod_type: #{clz}"
231
+ :nil
232
+ end
233
+ end
234
+
235
+ def mod_name_from_class(clz)
236
+ mod_name = clz.name
237
+ mod_name = clz.to_s unless check_name(mod_name)
238
+
239
+ undecorate_module_name(mod_name)
240
+ end
241
+
242
+ def localize_path(path)
243
+ # @app_root_path.relative_path_from(Pathname.new(path).cleanpath).to_s
244
+ return '' unless ChaosUtils.aught?(path)
245
+
246
+ p = Pathname.new(path).cleanpath.to_s
247
+ p.sub!(@app_root_path, '') if @app_root_path
248
+ local_path = p.start_with?('/') ? p[1..-1] : p
249
+ local_path.to_s
250
+ end
251
+
252
+ def log(msg, **opts)
253
+ ChaosUtils.log_msg(msg, subject: 'Tracker', **opts)
254
+ end
255
+
256
+ def trace_mod_details(tp, label: 'ModDetails')
257
+ log format('Tracepoint [%s] (%s): %s / %s [%s / %s]', label, tp.event, tp.defined_class, tp.self.class, tp.defined_class&.name, tp.self.class&.name)
258
+ end
259
+
260
+ def check_name(mod_nm)
261
+ ChaosUtils.aught?(mod_nm) && !mod_nm.strip.start_with?('#')
262
+ end
263
+
264
+ def safe_mod_info(mod_name, mod_type, mod_full_path)
265
+ return nil if full_path_skip?(mod_full_path)
266
+ return nil if module_skip?(mod_name)
267
+ # puts ['mod_full_path', mod_full_path].inspect
268
+
269
+ ChaosDetector::Stacker::ModInfo.new(
270
+ mod_name: mod_name,
271
+ mod_path: localize_path(mod_full_path),
272
+ mod_type: mod_type
273
+ )
274
+ end
275
+ end
276
+ end