chaos_detector 0.4.9

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.
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