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