excadg 0.2.3 → 0.2.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bccdde832f42e296103812d1a60fa0adf8534d102c697d08b97697d5524dd37
4
- data.tar.gz: a1da774573729eac556f4dfa3ff2c46bb21f1029db30cebeb172e00c9f3f9460
3
+ metadata.gz: 1b39410e76cbbe770fa2baa0f94eb10f25dd73bb644bb304c19fe4bc7b7fa39b
4
+ data.tar.gz: 0df53fa109fbfdb56809b87d8dd8314c5f884bb6a8060ce8eff2913627ca63c1
5
5
  SHA512:
6
- metadata.gz: 29923112c768f2bb6a3ca99109c5d2a7bd48b6b0b45b9df7c9715ab29f557414fce338da3dd7ff6cbda5922d10bfd04d57316204c2279b8f402dba08687306f3
7
- data.tar.gz: 496d09bc1390c268800976e6aa238f153671c9548d40be92e82e6d12b5a949537070317b65973ddbbfe62e6783b9643cf19280ea72a333ff577e0e6dc89cd67f
6
+ metadata.gz: 897dab58f102d0baf617b8d592f46a649e9c4213186cad003f79a36711c104d7d393a6f6e1454119465fbb423c0c4be6f980f89334d4ac5c77818b5b7ed718a5
7
+ data.tar.gz: 1cf7919dd79fed3d3e21499b7a246a9a96ebec26b06cb1462abdb66db67ab971de5f639ffc9ca00c025be1b313bec5fcd0ecb9e26147f95e437e1a36e8ff56ce
data/README.md CHANGED
@@ -185,12 +185,17 @@ Logging is disabled by default, but it could be useful to debug tests. Add `ExcA
185
185
  - problem: can't find what to suspend
186
186
  - make a loop payload template
187
187
  - provide a mechanism to control # of children
188
-
189
- ## Graph checks
190
-
191
- - check for loops in the config
188
+ - allow to dump shell payload stdout/err in realtime
189
+
190
+ ## Graph stability and tracking
191
+
192
+ - add a tracker that collects execution graph from broker
193
+ - is hooked by broker
194
+ - can be enabled / disabled
195
+ - visualize current execution on TUI
196
+ - allow to focus on a certain vertex to see what's it waiting for
197
+ - allow to dump currect vertice's state
198
+ - implement checks using tracker:
199
+ - check for loops by deducting vertices to some signatures
192
200
  - check for unreachable islands - graph connectivity
193
201
  - check that there are nodes to start from
194
-
195
-
196
- - check MAX_CPU ...
@@ -5,6 +5,9 @@ module ExcADG
5
5
  module Assertions
6
6
  class << self
7
7
  # asserts that all vars are instances of one of the clss
8
+ # @param vars array or a single variable to check
9
+ # @param clss array or a single class to check against
10
+ # @raise StandardError if any of vars are not of clss
8
11
  def is_a? vars, clss
9
12
  return if vars.is_a?(Array) && clss == Array
10
13
 
data/lib/excadg/broker.rb CHANGED
@@ -11,7 +11,7 @@ module ExcADG
11
11
  # handle requests sending/receiving though Ractor's interface
12
12
  module Broker
13
13
  class << self
14
- attr_reader :data_store
14
+ attr_reader :data_store, :vtracker
15
15
 
16
16
  # is used from vertices to send reaqests to the broker
17
17
  # @return data received in response from the main ractor
@@ -36,9 +36,11 @@ module ExcADG
36
36
  end
37
37
 
38
38
  # start requests broker for vertices in a separated thread
39
+ # @param track {Boolean} whether to track execution graph
39
40
  # @return the thread started
40
- def run
41
+ def run track: false
41
42
  @data_store ||= DataStore.new
43
+ @vtracker ||= track ? VTracker.new : nil
42
44
  @broker = Thread.new { loop { process_request } } unless @broker&.alive?
43
45
 
44
46
  at_exit {
@@ -79,6 +81,7 @@ module ExcADG
79
81
  Log.info "received request: #{request}"
80
82
  request.self.send case request
81
83
  when Request::GetStateData
84
+ @vtracker&.track request.self, deps
82
85
  request.filter? ? request.deps.collect { |d| @data_store[d] } : @data_store.to_a
83
86
  when Request::Update
84
87
  @data_store << request.data
@@ -35,8 +35,8 @@ module ExcADG
35
35
  @by_vertex[new.vertex] = new if new.vertex
36
36
  end
37
37
 
38
- # retrieves VStateData by key
39
- # @param key either Vertex or VStateData::Key to retrieve Full state data
38
+ # retrieves {VStateData} by key
39
+ # @param key {Vertex} or {VStateData::Key} or vertex name (String || Symbol) to retrieve Full state data
40
40
  # @return VStateData::Full for the respective key
41
41
  # @raise StandardError if key is not of a supported type
42
42
  def [] key
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # static assets to select from
4
+ module ExcADG::Tui
5
+ module Assets
6
+ CORNERS = {
7
+ sharp: '┏┓┗┛',
8
+ round: '╭╮╰╯'
9
+ }.freeze
10
+ end
11
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'format'
4
+
5
+ module ExcADG::Tui
6
+ # basic TUI building block;
7
+ # in a nutshell, it's a column, as {String}s in it are printed vertically one by one (see {#to_s});
8
+ # it could be a row only semantically if it's a part of a vertically composed array (see {.column})
9
+ class Block
10
+ attr_reader :width, :array
11
+
12
+ include Format
13
+
14
+ # transform array or "rows" to a single column;
15
+ # @example
16
+ # Block.column "some", "other" # some and other will be centered in column by default
17
+ # Block.column "some", "other", align: :left # some and other will be shifted to the left
18
+ # Block.column
19
+ # Block.row("one", "two"),
20
+ # "three"
21
+ # ]} # one and two will be printed in the first row, three will be below them centered
22
+ # Block.column("some", "other") { |el| el.box! } # enclose both string to boxes before making a row
23
+ # @param *rows array of rows ({Block}s / {String}s)
24
+ # @param align how to align blocks between each other: :center (default), :right, :left
25
+ # @param &block idividual row processor, the block is supplied with {Block}s
26
+ def self.column *rows, align: nil, &block
27
+ # row could be a String, make an array of horizontal lines from it
28
+ rows.collect! { |col| col.is_a?(Block) ? col : Block.new(col) }
29
+ rows.collect!(&block) if block_given? # allow to pre-process "rows"
30
+ max_row_width = rows.collect(&:width).max
31
+ Block.new rows.collect! { |blk|
32
+ extra_columns = max_row_width - blk.width
33
+ case align
34
+ when :left then blk.collect! { |line| line + ' ' * extra_columns }
35
+ when :right then blk.collect! { |line| ' ' * extra_columns + line }
36
+ else
37
+ blk.h_pad!(extra_columns / 2)
38
+ extra_columns.odd? ? blk.collect! { |line| line + ' ' } : blk
39
+ end
40
+ blk.array # get the array to join using builtin flatten
41
+ }.flatten!
42
+ end
43
+
44
+ # squash a row of columns (blocks) to a single block
45
+ # @example
46
+ # Block.row "some", "other" # => "someother"
47
+ # Block.row "some", ["other", "foo"], aligh: bottom # some will be shifted down by 1 line
48
+ # Block.row("some", "other") { |blk| blk.box! } # both columns will be enclosed in a box
49
+ # @param *cols array of columns ({Block}s / {String}s)
50
+ # @param align how to align blocks between each other: :center (default), :top, :bottom
51
+ # @param &block idividual column processor, the block is supplied with {Block}s
52
+ def self.row *cols, align: nil, &block
53
+ cols.collect! { |col| col.is_a?(Block) ? col : Block.new(col) }
54
+ cols.collect!(&block) if block_given? # allow to pre-process columns
55
+ max_col_height = cols.collect(&:height).max
56
+ Block.new cols.collect! { |col|
57
+ extra_lines = max_col_height - col.height
58
+ case align
59
+ when :top then col << Array.new(extra_lines, '')
60
+ when :bottom then col >> Array.new(extra_lines, '')
61
+ else
62
+ col.v_pad!(extra_lines / 2)
63
+ col << '' if extra_lines.odd?
64
+ end
65
+ col.v_align! # is needed due to transpose call below
66
+ col.array # get the array to process using builtin methods
67
+ }.transpose.collect(&:join)
68
+ end
69
+
70
+ # make printable
71
+ def to_s
72
+ @array.join "\n"
73
+ end
74
+
75
+ # add extra lines from the supplied array to the block;
76
+ # no auto-alignment is performed, see {#v_align} to make width even
77
+ # @param other either {Array} or {String} to push back
78
+ def << other
79
+ other.is_a?(Array) ? @array += other : @array << other
80
+ @width = @array.collect(&:size).max
81
+ self
82
+ end
83
+
84
+ # add extra lines to the start of the block
85
+ # @param other either {Array} or {String} to push forward
86
+ # @example
87
+ # Block.column '1'
88
+ # block << %w[2 3] # now block has %w[2 3 1]
89
+ def >> other
90
+ case other
91
+ when Array
92
+ other.reverse_each { |i|
93
+ @array.unshift i
94
+ }
95
+ when String
96
+ @array.unshift other
97
+ end
98
+ @width = @array.collect(&:size).max
99
+ self
100
+ end
101
+
102
+ # column's height is its size
103
+ def height
104
+ @array.size
105
+ end
106
+
107
+ # main inline modification method
108
+ def collect! &block
109
+ @array.collect!(&block)
110
+ @width = @array.collect(&:size).max
111
+ end
112
+
113
+ private
114
+
115
+ # constructor is private;
116
+ # {#column} and {#row} are enough to make blocks;
117
+ # in case you need to align a single block, use e.g. `Block.column("one", "two") { |blk| blk.box!.pad! 2 }`
118
+ def initialize arg
119
+ case arg
120
+ when Array then @array = arg
121
+ when String then @array = [arg]
122
+ else raise "can't make block from #{arg.class}"
123
+ end
124
+ @width = @array.collect(&:size).max
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExcADG::Tui
4
+ # collection of low-level {Block} formatting methods;
5
+ # most methods mutate an object they called for to avoid re-copying data each time;
6
+ # the module isn't expected to be used directly and exists just to de-couple
7
+ # formatting methods and basic blocks positioning (see {Block});
8
+ # all methods could be chained: `block.v_pad!(10).h_pad!(2).box!`
9
+ module Format
10
+ # add horizontal padding to the block
11
+ # @param size number of spaces to add
12
+ def h_pad! size
13
+ @array.collect! { |row|
14
+ "#{' ' * size}#{row}#{' ' * size}"
15
+ }
16
+ @width += size * 2
17
+ self
18
+ end
19
+
20
+ # add vertical padding to the block
21
+ # @param size number of spaces to add
22
+ def v_pad! size
23
+ filler = ' ' * @width
24
+ size.times {
25
+ self >> filler
26
+ self << filler
27
+ }
28
+ self
29
+ end
30
+
31
+ # adds spaces around the block
32
+ # @param size number of spaces to add
33
+ def pad! size
34
+ v_pad! size
35
+ h_pad! size
36
+ end
37
+
38
+ # aligns block elements vertically by adding spaces;
39
+ # all lines in block gets changed to have the same # of chars
40
+ # @param type {Symbol} :left (default), :center, :right
41
+ # @param width {Integer} target block width, defaults to the current width
42
+ def v_align! type = nil, width: nil
43
+ line_transformer = case type
44
+ when :center
45
+ ->(line, num_spaces) { ' ' * (num_spaces / 2) + line.to_s + (' ' * (num_spaces / 2)) + (num_spaces.odd? ? ' ' : '') }
46
+ when :right
47
+ ->(line, num_spaces) { (' ' * num_spaces) + line.to_s }
48
+ else # :left
49
+ ->(line, num_spaces) { line.to_s + (' ' * num_spaces) }
50
+ end
51
+
52
+ @width = width unless width.nil? || @width > width
53
+ @array.collect! { |line| line_transformer.call line, @width - line.size }
54
+ self
55
+ end
56
+
57
+ # adds a square box around the block;
58
+ # auto-aligns the block, so use {#v_align!}
59
+ # if you want custom alignment for the block
60
+ def box! corners: :round
61
+ corners = Assets::CORNERS[corners]
62
+ v_align!
63
+ @array.collect! { |line| "│#{line}│" }
64
+ @array.unshift "#{corners[0]}#{'─' * width}#{corners[1]}"
65
+ @array << "#{corners[2]}#{'─' * width}#{corners[3]}"
66
+ @width += 2
67
+ self
68
+ end
69
+
70
+ # fit the current block to a rectangle by
71
+ # cropping the block and adding a special markers to content;
72
+ # actual content width and height will be 1 char less to store cropping symbols;
73
+ # filling does not align content, {v_align!} does
74
+ # @param width width to fit, nil means don't touch width
75
+ # @param height height to fit, nil means don't touch height
76
+ # @param fill whether to fill column for the sizes provided
77
+ def fit! width: nil, height: nil, fill: false
78
+ # pre-calc width to use below
79
+ @width = width unless width.nil? || (@width < width && !fill)
80
+
81
+ unless height.nil?
82
+ if @array.size > height
83
+ @array.slice!((height - 1)..)
84
+ @array << ('░' * @width)
85
+ elsif fill && @array.size < height
86
+ @array += Array.new(height - @array.size, ' ' * @width)
87
+ end
88
+ end
89
+ unless width.nil?
90
+ @array.collect! { |line|
91
+ if line.size > width
92
+ "#{line[...(width - 1)]}░"
93
+ elsif fill && line.size < width
94
+ line << ' ' * (width - line.size)
95
+ else
96
+ line
97
+ end
98
+ }
99
+ end
100
+ self
101
+ end
102
+ end
103
+ end
data/lib/excadg/tui.rb CHANGED
@@ -1,11 +1,13 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'date'
4
2
  require 'io/console'
5
3
 
6
4
  require_relative 'broker'
7
5
  require_relative 'state_machine'
8
6
 
7
+ require_relative 'tui/assets'
8
+ require_relative 'tui/block'
9
+ require_relative 'tui/format'
10
+
9
11
  module ExcADG
10
12
  # render status on the screen
11
13
  module Tui
@@ -21,7 +23,10 @@ module ExcADG
21
23
  Log.info 'spawning tui'
22
24
  @thread = Thread.new {
23
25
  loop {
24
- print_in_box stats
26
+ # print_in_box stats
27
+ clear
28
+ refresh_sizes
29
+ print stats
25
30
  sleep DELAY
26
31
  }
27
32
  }
@@ -29,43 +34,32 @@ module ExcADG
29
34
 
30
35
  def summarize has_failed, timed_out
31
36
  @thread.kill
32
- print_in_box stats + (print_summary has_failed, timed_out)
33
- end
34
-
35
- private
36
-
37
- # @param content is a list of lines to print
38
- def print_in_box content
39
37
  clear
40
- refresh_sizes
41
- print "+-#{'-' * @content_size[:width]}-+\n"
42
- content[..@content_size[:height]].each { |line|
43
- if line.size > @content_size[:width]
44
- printf @line_template, "#{line[...(@content_size[:width] - 3)]}..."
45
- else
46
- printf @line_template, line
47
- end
48
- }
49
- if content.size < @content_size[:height]
50
- (@content_size[:height] - content.size).times { printf @line_template, ' ' }
51
- else
52
- printf @line_template, '<some content did not fit and was cropped>'[..@content_size[:width]]
53
- end
54
- print "+-#{'-' * @content_size[:width]}-+\n"
38
+ print stats summary: get_summary(has_failed, timed_out)
55
39
  end
56
40
 
57
- def print_summary has_failed, timed_out
41
+ # private
42
+
43
+ def get_summary has_failed, timed_out
58
44
  [timed_out ? 'execution timed out' : 'execution completed',
59
45
  "#{has_failed ? 'some' : 'no'} vertices failed"]
60
46
  end
61
47
 
62
48
  # make summary paragraph on veritces
63
- def stats
64
- [
65
- "time spent (ms): #{DateTime.now.strftime('%Q').to_i - @started_at}",
66
- "vertices seen: #{Broker.data_store.size}",
67
- 'progress:'
68
- ] + state_stats.collect { |line| " #{line}" }
49
+ def stats summary: nil
50
+ Block.column(
51
+ Block.column(summary || 'running').h_pad!(1).box!.v_align!(:center, width: @content_size[:width]),
52
+ Block.column(
53
+ *[
54
+ "time spent (ms): #{DateTime.now.strftime('%Q').to_i - @started_at}",
55
+ "vertices seen: #{Broker.data_store.size}",
56
+ 'progress:'
57
+ ] + state_stats,
58
+ align: :left
59
+ ).h_pad!(2),
60
+ align: :left
61
+ ).fit!(width: @content_size[:width], height: @content_size[:height], fill: true)
62
+ .box!(corners: :sharp)
69
63
  end
70
64
 
71
65
  def clear
@@ -81,7 +75,7 @@ module ExcADG
81
75
  height: box_size[:height] - 4, # 2 for borders, 1 for \n, 1 for remark
82
76
  width: box_size[:width] - 5 # 2 for borders, 2 to indent
83
77
  }.freeze
84
- @line_template = "| %-#{@content_size[:width]}s |\n"
78
+ @line_template = " %-#{@content_size[:width]}s │\n"
85
79
  end
86
80
 
87
81
  # make states summary, one for a line with consistent placing
@@ -93,13 +87,34 @@ module ExcADG
93
87
  .collect { |state, vertices| [state, vertices_stats(vertices)] }
94
88
  .to_h
95
89
  # rubocop:enable Style/HashTransformValues
96
- filled.collect { |k, v| format '%-10s: %s', k, "#{v.empty? ? '<none>' : v}" }
90
+ filled.collect { |k, v| format ' %-10s: %s', k, "#{v.empty? ? '<none>' : v}" }
97
91
  end
98
92
 
99
93
  def vertices_stats vertice_pairs
100
- full_list = vertice_pairs.collect(&:name)
101
- addition = full_list.size > MAX_VERTEX_TO_SHOW ? "... and #{full_list.size - MAX_VERTEX_TO_SHOW} more" : ''
102
- full_list[0...MAX_VERTEX_TO_SHOW].join(', ') + addition
94
+ vertice_pairs.collect(&:name).join(', ')
95
+ end
96
+
97
+ # make ASCII drawing of a graph
98
+ def tracked_graph g, r
99
+ reversed_graph = g # Broker.vtracker.graph.reverse
100
+ # Broker.vtracker.by_state[:ready].collect { |ready_vertex|
101
+ items = r.collect { |ready_vertex|
102
+ # deps = g.adjacent_vertices ready_vertex
103
+ # parents = reversed_graph.adjacent_vertices ready_vertex
104
+ [ready_vertex.name.to_s, ready_vertex.state.to_s]
105
+ }
106
+ items[1] << 'asd'
107
+ items[1] << 'dsa'
108
+ Block.row(
109
+ Block.column(
110
+ Block.row(*items) { |blk| blk.v_align!(:right).box!.h_pad!(3) },
111
+ ['x' * 70],
112
+ align: :centre
113
+ ),
114
+ Block.column(*Array.new(20, 'Y')),
115
+ align: :bottom
116
+ ).array
117
+ # TODO: add a row below the current row
103
118
  end
104
119
  end
105
120
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgl/adjacency'
4
+
5
+ module ExcADG
6
+ # tracker for {Vertex}-es graph:
7
+ # it's hooked by {Broker} to register dependencies polling events
8
+ # and make an actual graph of vertices with states in runtime
9
+ #
10
+ # it's not possible to do this in other way, because vertices can be spawned:
11
+ # - in any order => code is not guaranteed to know the full graph (even static one)
12
+ # - dynamically => there is no central place but {Broker} that's aware of all vertices
13
+ class VTracker
14
+ attr_reader :graph, :by_state
15
+
16
+ def initialize
17
+ @graph = RGL::DirectedAdjacencyGraph.new
18
+ @by_state = {}
19
+ end
20
+
21
+ # register the vertex and its new known deps in the @graph and by_state cache
22
+ # @param vertice vertice that requested info about deps
23
+ # @param deps list of dependencies as supplied by {Request::GetStateData}
24
+ def track vertex, deps
25
+ Assertions.is_a? vertex, Vertex
26
+ Assertions.is_a? deps, Array
27
+
28
+ add_to_states_cache vertex, vertex.state
29
+
30
+ deps.each { |raw_dep|
31
+ # it could be not Vertex, so do a lookup through data store
32
+ next unless Broker.data_store[raw_dep]
33
+
34
+ dep_data = Broker.data_store[raw_dep]
35
+ add_to_states_cache dep_data.vertex, dep_data.state
36
+ @graph.add_edge vertex, dep_data.vertex
37
+ }
38
+ end
39
+
40
+ private
41
+
42
+ # adds a given {Vertex} in state to @by_state cache;
43
+ # makes sure to remove it from the rest of the lists;
44
+ # lazily initializes the cache
45
+ def add_to_states_cache vertex, state
46
+ # TODO: shouldn't we add these vertices to the graph as "unresolved" up until they appear as {Vertex}
47
+ return unless state
48
+
49
+ @by_state.each_value { |v| v.delete vertex }
50
+ @by_state[state] ||= []
51
+ @by_state[state] << vertex
52
+ end
53
+ end
54
+ end
data/lib/excadg.rb CHANGED
@@ -15,3 +15,4 @@ require_relative 'excadg/state_machine'
15
15
  require_relative 'excadg/tui'
16
16
  require_relative 'excadg/vertex'
17
17
  require_relative 'excadg/vstate_data'
18
+ require_relative 'excadg/vtracker'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excadg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - skorobogatydmitry
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-13 00:00:00.000000000 Z
11
+ date: 2024-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rgl
@@ -51,9 +51,13 @@ files:
51
51
  - lib/excadg/rtimeout.rb
52
52
  - lib/excadg/state_machine.rb
53
53
  - lib/excadg/tui.rb
54
+ - lib/excadg/tui/assets.rb
55
+ - lib/excadg/tui/block.rb
56
+ - lib/excadg/tui/format.rb
54
57
  - lib/excadg/vertex.rb
55
58
  - lib/excadg/vstate_data.rb
56
59
  - lib/excadg/vtimeout.rb
60
+ - lib/excadg/vtracker.rb
57
61
  homepage: https://github.com/skorobogatydmitry/excadg
58
62
  licenses:
59
63
  - LGPL-3.0-only