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,117 @@
1
+ # naught?("")
2
+ # naught?(0)
3
+ # naught?([])
4
+ # naught?("foobar")
5
+ # naught?(0)
6
+ # naught?([])
7
+ # module ChaosDetector
8
+
9
+ # = ChaosDetector::Utils::CoreUtil::with
10
+ module ChaosDetector
11
+ module Utils
12
+ module CoreUtil
13
+ class AssertError < StandardError; end
14
+
15
+ class << self
16
+ def enum(*values)
17
+ Module.new do |mod|
18
+ values.each_with_index do |v, _i|
19
+ mod.const_set(v.to_s.upcase, v.to_s.downcase.to_sym)
20
+ # mod.const_set(v.to_s.upcase, 2**i)
21
+ end
22
+
23
+ def mod.values
24
+ constants
25
+ end
26
+ end
27
+ end
28
+
29
+ def naught?(obj)
30
+ if obj.nil?
31
+ true
32
+ elsif obj.is_a?(FalseClass)
33
+ true
34
+ elsif obj.is_a?(TrueClass)
35
+ false
36
+ elsif obj.is_a?(String)
37
+ obj.strip.empty?
38
+ elsif obj.is_a?(Enumerable)
39
+ obj.none? { |o| aught?(o) }
40
+ elsif obj.is_a?(Numeric)
41
+ obj == 0
42
+ end
43
+ end
44
+
45
+ def aught?(obj)
46
+ !naught?(obj)
47
+ end
48
+
49
+ def with(obj, aught:false)
50
+ raise ArgumentError('with requires block') unless block_given?
51
+
52
+ yield obj if (aught ? aught?(obj) : !!obj)
53
+ end
54
+
55
+ def assert(expected_result=true, msg=nil, &block)
56
+ if block.nil? && !expected_result
57
+ raise AssertError, "Assertion failed. #{msg}"
58
+ elsif !block.nil? && block.call != expected_result
59
+ raise AssertError, "Assertion failed. #{msg}\n\t#{block.source_location}"
60
+ end
61
+ end
62
+
63
+ # @return subset of given properties not contained within given object
64
+ def properties_complement(props, obj:)
65
+ return props if obj.nil?
66
+ raise ArgumentError, 'props is required.' unless props
67
+
68
+ puts "(#{obj.class} )props: #{props}"
69
+
70
+ props - case obj
71
+ when Hash
72
+ puts 'KKKKK'
73
+ puts "obj.keys: #{obj.keys}"
74
+ obj.keys
75
+
76
+ else
77
+ puts "PPPPP #{obj.class}"
78
+ puts "obj.public_methods: #{obj.public_methods}"
79
+ obj.public_methods
80
+
81
+ end
82
+ end
83
+
84
+ # Built-in self-testing:
85
+ # ChaosDetector::Utils::CoreUtil.test
86
+ def test
87
+ puts('Testing ChaosDetector::Utils::CoreUtil')
88
+ assert(true, 'Naught detects blank string') {naught?('')}
89
+ assert(true, 'Naught detects blank string with space') {naught?(' ')}
90
+ assert(true, 'Naught detects int 0') {naught?(0)}
91
+ assert(true, 'Naught detects float 0.0') {naught?(0.0)}
92
+ assert(true, 'Naught detects empty array') {naught?([])}
93
+ assert(true, 'Naught detects empty hash') {naught?({})}
94
+ assert(false, 'Naught real string') {naught?('foobar')}
95
+ assert(false, 'Naught non-zero int') {naught?(1)}
96
+ assert(false, 'Naught non-zero float') {naught?(33.33)}
97
+ assert(false, 'Naught non-empty array') {naught?(['stuff'])}
98
+ assert(false, 'Naught non-empty hash') {naught?({ foo: 'bar' })}
99
+ end
100
+ end
101
+
102
+ module ChaosAttr
103
+ def chaos_attr(attribute_name, default_val=nil, &block)
104
+ # raise 'Default value or block required' unless !default_val.nil? || block
105
+ sym = attribute_name&.to_sym
106
+ raise ArgumentError, 'attribute_name is required and convertible to symbol.' if sym.nil?
107
+
108
+ define_method(sym) do
109
+ instance_variable_get("@#{sym}") || (block.nil? ? default_val : block.call)
110
+ end
111
+
112
+ define_method("#{sym}=") { |val| instance_variable_set("@#{sym}", val) }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,49 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require_relative 'core_util'
4
+
5
+ module ChaosDetector
6
+ module Utils
7
+ module FSUtil
8
+ class << self
9
+
10
+ # Relative path:
11
+ def rel_path(dir_path, from_path:)
12
+ pathname = Pathname.new(dir_path)
13
+ base_path = Pathname.new(from_path).cleanpath
14
+ pathname.relative_path_from(base_path).to_s
15
+ end
16
+
17
+ # Ensure directory and all its parents exist, like (mkdir -p):
18
+ def ensure_dirpath(dirpath)
19
+ raise ArgumentError, '#ensure_paths_to_file requires dirpath' if nay? dirpath
20
+
21
+ FileUtils.mkdir_p(dirpath)
22
+ end
23
+
24
+ # Ensure file's directory and all its parents exist, then write given string to it:
25
+ def safe_file_write(filepath, content: nil)
26
+ raise ArgumentError, '#write_to_file requires filepath' if nay? filepath
27
+
28
+ ensure_paths_to_file(filepath)
29
+ File.write(filepath, content)
30
+ filepath
31
+ end
32
+
33
+ # Ensure file's directory and all its parents exist, like (mkdir -p):
34
+ def ensure_paths_to_file(filepath)
35
+ raise ArgumentError, '#ensure_paths_to_file requires filepath' if nay? filepath
36
+
37
+ dirpath = File.split(filepath).first
38
+ raise "dirpath couldn't be obtained from #{filepath}" if nay? dirpath
39
+
40
+ ensure_dirpath(dirpath)
41
+ end
42
+
43
+ def nay?(obj)
44
+ ChaosDetector::Utils::CoreUtil.naught?(obj)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ module ChaosDetector
2
+ module Utils
3
+ module LerpUtil
4
+ class << self
5
+
6
+ def delerp(val, min:, max:)
7
+ return 0.0 if min==max
8
+ (val - min).to_f / (max - min)
9
+ end
10
+
11
+ # Linear interpolation between min and max:
12
+ # @arg pct is percentage where 1.0 represents 100%
13
+ def lerp(pct, min:, max:)
14
+ return 0.0 if min==max
15
+ (max-min) * pct.to_f + min
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'core_util'
2
+ require_relative 'str_util'
3
+
4
+ module ChaosDetector
5
+ module Utils
6
+ module LogUtil
7
+ class << self
8
+ # Simple logging, more later
9
+ def log(msg, object: nil, subject: nil)
10
+ # raise ArgumentError, "no message to log" if nay?(msg)
11
+ return if nay?(msg)
12
+
13
+ subj = d(subject, clamp: :brace)
14
+ obj = d(object, clamp: :bracket, prefix: ': ')
15
+ message = d(msg, prefix: subj, suffix: obj)
16
+
17
+ if block_given?
18
+ print_line(d(message, prefix: 'Starting: '))
19
+ result = yield
20
+ print_line(d(message, prefix: 'Complete: ', suffix: d(result)))
21
+ else
22
+ print_line(message)
23
+ end
24
+ message
25
+ end
26
+
27
+ def print_line(msg, *opts)
28
+ # print("#{msg}\n", opts)
29
+ # nil
30
+ Kernel.puts(msg, opts)
31
+ end
32
+
33
+ alias pp print_line
34
+
35
+ def nay?(obj)
36
+ ChaosDetector::Utils::CoreUtil.naught?(obj)
37
+ end
38
+
39
+ def d(text, *args)
40
+ ChaosDetector::Utils::StrUtil.decorate(text, *args)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,90 @@
1
+ require_relative 'core_util'
2
+ module ChaosDetector
3
+ module Utils
4
+ module StrUtil
5
+ STR_INDENT = ' '.freeze
6
+ STR_BLANK = ''.freeze
7
+ STR_NS_SEP = '::'.freeze
8
+ SPACE = ' '.freeze
9
+ SCORE = '_'.freeze
10
+
11
+ class << self
12
+
13
+ def decorate_pair(source, dest, indent_length: 0, clamp: :angle, join_str: ' ')
14
+ decorate("#{decorate(source)}#{decorate(dest, prefix: join_str)}", clamp: clamp, indent_length: indent_length)
15
+ end
16
+
17
+ def decorate_tuple(tuple, indent_length: 0, clamp: :angle, join_str: ' ')
18
+ body = tuple.map { |t| decorate(t, indent_length: indent_length)}.join(join_str)
19
+ decorate(body, clamp: clamp, indent_length: indent_length)
20
+ end
21
+
22
+ def decorate(text, clamp: :nil, prefix: nil, suffix: nil, sep: nil, indent_length: 0)
23
+ return '' if nay? text
24
+
25
+ clamp_pre, clamp_post = clamp_chars(clamp: clamp)
26
+ indent("#{prefix}#{sep}#{clamp_pre}#{text}#{clamp_post}#{sep}#{suffix}", indent_length)
27
+ end
28
+
29
+ def humanize_module(mod_name, max_segments: 2, sep_token: STR_NS_SEP)
30
+ return '' if nay? mod_name
31
+ raise ArgumentError, 'Must have at least 1 segment.' if max_segments < 1
32
+
33
+ mod_name.split(sep_token).last(max_segments).join(sep_token)
34
+ end
35
+
36
+ def snakeize(obj)
37
+ obj.to_s.gsub(/[^a-zA-Z\d\s:]/, SCORE)
38
+ end
39
+
40
+ def blank?(obj)
41
+ obj.nil? || obj.to_s.empty?
42
+ end
43
+
44
+ def squish(str)
45
+ str.to_s.strip.split.map(&:strip).join(SPACE)
46
+ end
47
+
48
+ def titleize(obj)
49
+ obj.to_s.split(SCORE).map(&:capitalize).join(SPACE)
50
+ end
51
+
52
+ alias d decorate
53
+
54
+ def clamp_chars(clamp: :none)
55
+ case clamp
56
+ when :angle, :arrow
57
+ ['<', '>']
58
+ when :brace
59
+ ['{', '}']
60
+ when :bracket
61
+ ['[', ']']
62
+ when :italic, :emphasize
63
+ %w[_ _]
64
+ when :strong, :bold, :stars
65
+ ['**', '**']
66
+ when :quotes, :double_quotes
67
+ ['"', '"']
68
+ when :ticks, :single_quotes
69
+ ["'", "'"]
70
+ when :none
71
+ [STR_BLANK, STR_BLANK]
72
+ else # :parens, :parentheses
73
+ ['(', ')']
74
+ end
75
+ end
76
+
77
+ def indent(text, indent_length=1)
78
+ return '' if nay? text
79
+ return text unless indent_length
80
+
81
+ "#{STR_INDENT * indent_length}#{text}"
82
+ end
83
+
84
+ def nay?(obj)
85
+ ChaosDetector::Utils::CoreUtil.naught?(obj)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,21 @@
1
+ require 'matrix'
2
+ require_relative 'lerp_util'
3
+ module ChaosDetector
4
+ module Utils
5
+ module TensorUtil
6
+ class << self
7
+ # Return new matrix that is normalized from 0.0(min) to 1.0(max)
8
+ def normalize_matrix(matrix)
9
+ mag = matrix.row_size
10
+ raise ArgumentError if matrix.column_size != mag
11
+
12
+ lo, hi = matrix.minmax
13
+
14
+ Matrix.build(mag) do |row, col|
15
+ ChaosDetector::Utils::LerpUtil.delerp(matrix[row, col], min: lo, max: hi)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,214 @@
1
+ require 'csv'
2
+ require 'digest'
3
+
4
+ require 'chaos_detector/utils/fs_util'
5
+ require 'chaos_detector/chaos_utils'
6
+
7
+ require_relative 'options'
8
+ require_relative 'stacker/frame'
9
+
10
+ # TODO: add traversal types to find depth, coupling in various ways (directory/package/namespace):
11
+ module ChaosDetector
12
+ class Walkman
13
+ PLAYBACK_MSG = "Playback error on line number %d of pre-recorded CSV %s:\n %s\n %s".freeze
14
+ CSV_HEADER = %w[ROWNUM EVENT MOD_NAME MOD_TYPE FN_PATH FN_LINE FN_NAME CALLER_TYPE CALLER_PATH CALLER_INFO CALLER_NAME].freeze
15
+ COL_COUNT = CSV_HEADER.length
16
+ COL_INDEXES = CSV_HEADER.map.with_index { |col, i| [col.downcase.to_sym, i]}.to_h
17
+
18
+ DEFALT_BUFFER_LENGTH = 1000
19
+
20
+ def initialize(options:)
21
+ @buffer_length = options.walkman_buffer_length || DEFALT_BUFFER_LENGTH
22
+ @options = options
23
+ flush_csv
24
+ @csv_path = nil
25
+ @log_buffer = []
26
+ @rownum = 0
27
+ end
28
+
29
+ def record_start
30
+ flush_csv
31
+ @csv_path = nil
32
+ @log_buffer = []
33
+ @rownum = 0
34
+ autosave_csv
35
+ init_file_with_header(csv_path)
36
+ end
37
+
38
+ # Return frame at given index or nil if nothing:
39
+ def frame_at(row_index:)
40
+ frames_within(row_range: row_index..row_index).first
41
+ end
42
+
43
+ # Return any frames within specified row range; empty array if not found:
44
+ def frames_within(row_range: nil)
45
+ to_enum(:playback, row_range: row_range).map { |_r, frame| frame }
46
+ end
47
+
48
+ # Play back CSV from path configured in Walkman options
49
+ # @param row_range optionally specify range of rows. Leave nil for all.
50
+ # yields each row as
51
+ # frame A Frame object with its attributes contained in the CSV row
52
+ def playback(row_range: nil)
53
+ started_at = Time.now
54
+
55
+ if row_range
56
+ log('Walkman replaying CSV with (row_range) %d/%d lines: %s' % [
57
+ [count, row_range.max].min,
58
+ count,
59
+ csv_path
60
+ ])
61
+ else
62
+ log("Walkman replaying CSV with #{count} lines: #{csv_path}")
63
+ end
64
+
65
+ @rownum = 0
66
+ row_cur = nil
67
+ CSV.foreach(csv_path, headers: true) do |row|
68
+ row_cur = row
69
+ yield @rownum, playback_row(row) if row_range.nil? || row_range.include?(@rownum)
70
+ @rownum += 1
71
+ break if row_range && @rownum > row_range.max
72
+ end
73
+ rescue StandardError => e
74
+ log("%s:\n%s" % [e.to_s, e.backtrace.join("\n")])
75
+ raise ScriptError, log(format(PLAYBACK_MSG, @rownum, csv_path, row_cur, e.inspect))
76
+ ensure
77
+ log('Replayed %d items in %.2f seconds' % [@rownum, Time.now - started_at])
78
+ end
79
+
80
+ def count
81
+ `wc -l #{csv_path}`.to_i
82
+ end
83
+
84
+ # Call when done to flush any buffers as necessary
85
+ def stop
86
+ flush_csv
87
+ log("Stopped with #{count} lines.")
88
+ self
89
+ end
90
+
91
+ def csv_path
92
+ @csv_path ||= @options.path_with_root(key: :frame_csv_path)
93
+ end
94
+
95
+ def autosave_csv
96
+ csvp = csv_path
97
+ if FileTest.exist?(csvp)
98
+ 1.upto(100) do |n|
99
+ p = "#{csvp}.#{n}"
100
+ next if FileTest.exist?(p)
101
+
102
+ log("Autosaving #{csvp} to #{p}")
103
+ cp_result = `cp #{csvp} #{p}`
104
+ log(cp_result)
105
+ break
106
+ end
107
+ end
108
+ end
109
+
110
+ def buffered_trigger
111
+ if @log_buffer.length > @buffer_length
112
+ # log("Triggering flush @#{@log_buffer.length} / @buffer_length")
113
+ flush_csv
114
+ end
115
+ end
116
+
117
+ def log(msg, **opts)
118
+ ChaosUtils.log_msg(msg, subject: 'Walkman', **opts)
119
+ end
120
+
121
+ def flush_csv
122
+ # log("Flushing...")
123
+ if @log_buffer&.any?
124
+ CSV.open(csv_path, 'a') do |csv|
125
+ @log_buffer.each do |log_line|
126
+ csv << log_line
127
+ end
128
+ end
129
+ @log_buffer.clear
130
+ end
131
+ end
132
+
133
+ def write_frame(frame, frame_offset: nil)
134
+ csv_row = [@rownum]
135
+ csv_row.concat(frame_csv_fields(frame))
136
+
137
+ @log_buffer << csv_row
138
+ @rownum += 1
139
+ buffered_trigger
140
+ end
141
+
142
+ private
143
+
144
+ def csv_row_val(row, col_header)
145
+ r = COL_INDEXES[col_header]
146
+ raise ArgumentError, "#{col_header} not found in CSV_HEADER: #{CSV_HEADER}" if r.nil? || r < 0 || r > COL_COUNT
147
+
148
+ row[r]
149
+ end
150
+
151
+ def frame_csv_fields(f)
152
+ [
153
+ f.event,
154
+ f.mod_info&.mod_name,
155
+ f.mod_info&.mod_type,
156
+ f.fn_info.fn_path,
157
+ f.fn_info.fn_line,
158
+ f.fn_info.fn_name,
159
+ f.caller_info&.component_type,
160
+ f.caller_info&.path,
161
+ f.caller_info&.info,
162
+ f.caller_info&.name
163
+ ]
164
+ end
165
+
166
+ def init_file_with_header(filepath)
167
+ ChaosDetector::Utils::FSUtil.ensure_paths_to_file(filepath)
168
+ File.open(filepath, 'w') { |f| f.puts CSV_HEADER.join(',')}
169
+ end
170
+
171
+ # Play back a single given row
172
+ # returns the event and frame as described in #playback
173
+ def playback_row(row)
174
+ event = csv_row_val(row, :event)
175
+ fn_path = csv_row_val(row, :fn_path)
176
+ fn_line = csv_row_val(row, :fn_line)&.to_i
177
+
178
+ mod_info = ChaosDetector::Stacker::ModInfo.new(
179
+ mod_name: csv_row_val(row, :mod_name),
180
+ mod_path: fn_path,
181
+ mod_type: csv_row_val(row, :mod_type)
182
+ )
183
+
184
+ fn_info = ChaosDetector::Stacker::FnInfo.new(
185
+ fn_name: csv_row_val(row, :fn_name),
186
+ fn_line: fn_line,
187
+ fn_path: fn_path
188
+ )
189
+
190
+ caller_info = ChaosUtils.with(csv_row_val(row, :caller_type)) do |caller_type|
191
+ if caller_type.to_sym == :function
192
+ ChaosDetector::Stacker::FnInfo.new(
193
+ fn_name: csv_row_val(row, :caller_name),
194
+ fn_line: csv_row_val(row, :caller_info)&.to_i,
195
+ fn_path: csv_row_val(row, :caller_path)
196
+ )
197
+ else
198
+ ChaosDetector::Stacker::ModInfo.new(
199
+ mod_name: csv_row_val(row, :caller_name),
200
+ mod_path: csv_row_val(row, :caller_path),
201
+ mod_type: csv_row_val(row, :caller_info)
202
+ )
203
+ end
204
+ end
205
+
206
+ ChaosDetector::Stacker::Frame.new(
207
+ event: event.to_sym,
208
+ mod_info: mod_info,
209
+ fn_info: fn_info,
210
+ caller_info: caller_info
211
+ )
212
+ end
213
+ end
214
+ end