trailblazer-developer 0.0.26 → 0.0.28

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: 461fb0299b40474ecd967a538f7f8db2fe36a084bac62c8af1e309c3f0a52759
4
- data.tar.gz: c589095535ff6be6868b691c67069e4054024b028cbd2f1f68ea5f178a7de704
3
+ metadata.gz: 2bc71f425eee589bda27a8b681b796650443e688c3dc46ddb7d12e6daf092f08
4
+ data.tar.gz: 2f128bcf1675ba3147db4737f348b52d9a1f96562e317feeddda79846c1ca0e8
5
5
  SHA512:
6
- metadata.gz: 8bf357f48ef2132fd486f23f16d321eadbd4fcba82d117cb29082fe03be9aa832c02d837b9bcf48efb94fec29cd92b04f2d6cbf01cacf908e80a69483fc0aa4b
7
- data.tar.gz: 252980dc7e276c381b426fc8d8a73affef843a78386b5b771cf276af0ad6085ca0bba7752c25fdd637085dd791a69a70ab07140b576d01d815676cd3ba03520a
6
+ metadata.gz: 3b10040b636e96bc396ec89b41db298030552b0460ad7fa88b94ebf5a767e881cdf2c219ab573522f4226cb84d6e6d5baac786d384b7ef93d2788576dea4183f
7
+ data.tar.gz: 2eb0f50c4af74ab44ea0353e61b45a4645adb0f70132be92d329dd8123d0fd20561b58047be1bd9b2a25cb9e6bd5ccd2a6ac67089c7546e0041c78c24c455c62
@@ -5,13 +5,12 @@ jobs:
5
5
  strategy:
6
6
  fail-fast: false
7
7
  matrix:
8
- # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
9
- ruby: [2.5, 2.6, 2.7, '3.0', head, jruby]
8
+ ruby: [2.6, 2.7, '3.0', '3.1', '3.2', 'jruby']
10
9
  runs-on: ubuntu-latest
11
10
  steps:
12
- - uses: actions/checkout@v2
11
+ - uses: actions/checkout@v3
13
12
  - uses: ruby/setup-ruby@v1
14
13
  with:
15
14
  ruby-version: ${{ matrix.ruby }}
16
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
15
+ bundler-cache: true
17
16
  - run: bundle exec rake
data/CHANGES.md CHANGED
@@ -1,3 +1,38 @@
1
+ # 0.0.28
2
+
3
+ * Move `Introspect::Graph` over from `trailblazer-activity`. It's a data structure very specific
4
+ to rendering, which is not a part of pure runtime behavior.
5
+ * Use `Introspect.Nodes` API instead of `TaskMap`.
6
+ * Remove `:task_maps_per_activity` in `Debugger` as we don't need to cache anymore.
7
+ * Require `trailblazer-activity-dsl-linear-1.2.0`.
8
+ * Remove `Render::Circuit` as this is part of `trailblazer-activity` now.
9
+
10
+ # 0.0.27
11
+
12
+ ## Trace
13
+
14
+ * `Trace.capture_args` and `capture_return` no longer use any `Graph` logic. This
15
+ is moved to `Trace::Present` instead to reduce complexity at crunch time.
16
+ * Removed the `:focus_on` option for `#wtf?`. This is now solved through the debugger.
17
+ * `Trace.default_input_collector` and `default_output_collector` now receive the original taskWrap/pipeline-args
18
+ `[wrap_ctx, original_args]`.
19
+ * In `Trace::Present.call` it's now possible to provide labels to the `default_renderer` using the `:label` option.
20
+ * `Circuit.render` now accepts segments/path.
21
+ * Fixed a bug in `Trace::Present` where the traced top activity would show up as a node, which was wrong.
22
+ * Rename `Trace::Entity` to `Trace::Captured`.
23
+ * Removed `:position` keyword in `Trace::Renderer` and friends.
24
+ * Changed `Trace::Stack` from a nested datastructure to a linear `Captured` stack.
25
+
26
+ ## Debugger
27
+
28
+ * Add `Debugger::Node` as an interface and datastructure between tracing (`Stack` and `Captured`) and `Present`,
29
+ the latter now having access to `Debugger::Node`, only.
30
+ * Add `Debugger::Normalizer`, which computes default values for `Debugger::Node` at present-time.
31
+
32
+ ## Internals
33
+
34
+ * Use `trailblazer-activity-dsl-linear-1.0.0`.
35
+
1
36
  # 0.0.26
2
37
 
3
38
  * Fixing release, and allow using beta versions.
data/Gemfile CHANGED
@@ -3,8 +3,11 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in trailblazer-developer.gemspec
4
4
  gemspec
5
5
 
6
- # gem "trailblazer-activity", ">= 0.7.1"
7
- # gem "trailblazer-activity", path: "../trailblazer-activity"
8
- # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
6
+ # gem "trailblazer-activity", path: "../trailblazer-activity"
7
+ # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
8
+ # gem "trailblazer-operation", path: "../trailblazer-operation"
9
+ # gem "trailblazer-pro", path: "../trailblazer-pro"
10
+
11
+ # gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
9
12
  # gem "trailblazer-activity-dsl-linear", github: "trailblazer/trailblazer-activity-dsl-linear"
10
- # gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
13
+
@@ -0,0 +1,83 @@
1
+ # NOTE: The Graph API might get deprecated and replaced.
2
+ module Trailblazer
3
+ module Developer
4
+ module Introspect
5
+ # TODO: order of step/fail/pass in Node would be cool to have
6
+
7
+ # TODO: Remove Graph. This is only useful to render the full circuit
8
+ # Some thoughts here:
9
+ # * where do we need Schema.outputs? and where task.outputs?
10
+ #
11
+ #
12
+ # @private This API is still under construction.
13
+ class Graph
14
+ def initialize(activity)
15
+ @schema = activity.to_h or raise
16
+ @circuit = @schema[:circuit]
17
+ @map = @circuit.to_h[:map]
18
+ @configs = @schema[:nodes]
19
+ end
20
+
21
+ def find(id = nil, &block)
22
+ return find_by_id(id) unless block_given?
23
+
24
+ find_with_block(&block)
25
+ end
26
+
27
+ # TODO: convert to {#to_a}.
28
+ def collect(strategy: :circuit)
29
+ @map.keys.each_with_index.collect { |task, i| yield find_with_block { |node| node.task == task }, i }
30
+ end
31
+
32
+ def stop_events
33
+ @circuit.to_h[:end_events]
34
+ end
35
+
36
+ private
37
+
38
+ def find_by_id(id)
39
+ node = @configs.find { |_, _node| _node.id == id } or return
40
+ node_for(node[1])
41
+ end
42
+
43
+ def find_with_block
44
+ existing = @configs.find { |_, node| yield Node(node.task, node.id, node.outputs, node.data) } or return
45
+
46
+ node_for(existing[1])
47
+ end
48
+
49
+ # Build a {Graph::Node} with outputs etc.
50
+ def node_for(node_attributes)
51
+ Node(
52
+ node_attributes.task,
53
+ node_attributes.id,
54
+ node_attributes.outputs, # [#<struct Trailblazer::Activity::Output signal=Trailblazer::Activity::Right, semantic=:success>]
55
+ outgoings_for(node_attributes),
56
+ node_attributes.data,
57
+ )
58
+ end
59
+
60
+ def Node(*args)
61
+ Node.new(*args).freeze
62
+ end
63
+
64
+ Node = Struct.new(:task, :id, :outputs, :outgoings, :data)
65
+ Outgoing = Struct.new(:output, :task)
66
+
67
+ def outgoings_for(node)
68
+ outputs = node.outputs
69
+ connections = @map[node.task]
70
+
71
+ connections.collect do |signal, target|
72
+ output = outputs.find { |out| out.signal == signal }
73
+ Outgoing.new(output, target)
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.Graph(*args)
79
+ Graph.new(*args)
80
+ end
81
+ end # Graph
82
+ end
83
+ end
@@ -0,0 +1,17 @@
1
+ module Trailblazer
2
+ module Developer
3
+ module Introspect
4
+ # find the path for Strategy subclasses.
5
+ # FIXME: will be removed
6
+ def self.find_path(activity_class, segments)
7
+ activity = activity_class.is_a?(Class) ? activity_class.to_h[:activity] : activity_class # FIXME: not a real fan of this, maybe always do {to_h[:activity]}
8
+
9
+ node, activity, graph = Activity::Introspect.find_path(activity, segments)
10
+
11
+ activity = activity.is_a?(Class) ? activity.to_h[:activity] : activity # FIXME: not a real fan of this, maybe always do {to_h[:activity]}
12
+
13
+ return node, activity, graph
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,56 +2,13 @@ module Trailblazer
2
2
  module Developer
3
3
  module_function
4
4
 
5
- def render(activity, **options)
6
- Render::Circuit.(activity, **options)
7
- end
8
-
9
- module Render
10
- module Circuit
11
- module_function
12
-
13
- # Render an {Activity}'s circuit as a simple hash.
14
- def call(activity, **options)
15
- graph = Activity::Introspect::Graph(activity)
16
-
17
- circuit_hash(graph, **options)
18
- end
19
-
20
- def circuit_hash(graph, **options)
21
- content = graph.collect do |node|
22
- conns = node.outgoings.collect do |outgoing|
23
- " {#{outgoing.output.signal}} => #{inspect_with_matcher(outgoing.task, **options)}"
24
- end
25
-
26
- [ inspect_with_matcher(node.task, **options), conns.join("\n") ]
27
- end
28
-
29
- content = content.join("\n")
30
-
31
- "\n#{content}".gsub(/0x\w+/, "0x")
32
- end
33
-
34
- # If Ruby had pattern matching, this function wasn't necessary.
35
- def inspect_with_matcher(task, inspect_task: method(:inspect_task), inspect_end: method(:inspect_end))
36
- return inspect_task.(task) unless task.kind_of?(Trailblazer::Activity::End)
37
- inspect_end.(task)
38
- end
39
-
40
- def inspect_task(task)
41
- task.inspect
42
- end
43
-
44
- def inspect_end(task)
45
- class_name = Render::Circuit.strip(task.class)
46
- options = task.to_h
47
-
48
- "#<#{class_name}/#{options[:semantic].inspect}>"
49
- end
50
-
51
- def self.strip(string)
52
- string.to_s.sub("Trailblazer::Activity::", "")
53
- end
5
+ def render(activity, path: nil, **options)
6
+ if path # TODO: move to place where all renderers can use this logic!
7
+ node, _, graph = Developer::Introspect.find_path(activity, path)
8
+ activity = node.task
54
9
  end
10
+
11
+ Activity::Introspect::Render.(activity, **options)
55
12
  end
56
13
  end
57
14
  end
@@ -15,7 +15,7 @@ module Trailblazer
15
15
  module_function
16
16
 
17
17
  def call(operation, style: :line)
18
- graph = Activity::Introspect::Graph(operation)
18
+ graph = Introspect::Graph(operation)
19
19
 
20
20
  rows = graph.collect do |node, i|
21
21
  next if node[:data][:stop_event] # DISCUSS: show this?
@@ -0,0 +1,79 @@
1
+ module Trailblazer
2
+ module Developer
3
+ module Render
4
+ module TaskWrap
5
+ # @param activity Trailblazer::Activity
6
+ def self.render_for(activity, node)
7
+ task_wrap = task_wrap_for_activity(activity) # TODO: MERGE WITH BELOW
8
+ task = node.task
9
+ step_wrap = task_wrap[task] # the taskWrap for the actual step, e.g. {input,call_task,output}.
10
+
11
+ level = 2
12
+ nodes = render_pipeline(step_wrap, level)
13
+
14
+ nodes = [[0, activity], [1, node.id], *nodes]
15
+
16
+ Hirb::Console.format_output(nodes, class: :tree, type: :directory, multi_line_nodes: true)
17
+ end
18
+
19
+ # @param activity Activity
20
+ def self.task_wrap_for_activity(activity, **)
21
+ activity.to_h[:config][:wrap_static]
22
+ end
23
+
24
+ def self.render_pipeline(pipeline, level)
25
+ renderers = Hash.new(method(:render_task_wrap_step))
26
+ renderers.merge!(
27
+ Trailblazer::Activity::DSL::Linear::VariableMapping::Pipe::Input => method(:render_input),
28
+ Trailblazer::Activity::DSL::Linear::VariableMapping::Pipe::Output => method(:render_input),
29
+ )
30
+ # TODO: use collect
31
+ nodes=[]
32
+
33
+ pipeline.to_a.collect do |row|
34
+ renderer = renderers[row[1].class]
35
+
36
+ nodes = nodes + renderer.(row, level) # call the rendering component.
37
+ end
38
+
39
+ nodes
40
+ end
41
+
42
+ def self.render_task_wrap_step(row, level)
43
+ text = row.id.to_s.ljust(33, ".") + row[1].class.to_s
44
+
45
+ [[level, text]]
46
+ end
47
+
48
+ def self.render_input(row, level)
49
+ variable_mapping = Activity::DSL::Linear::VariableMapping
50
+
51
+ input_pipe = row[1].instance_variable_get(:@pipe) # this is again a {TaskWrap::Pipeline}.
52
+
53
+ filters = input_pipe.to_a.collect do |id, filter|
54
+ id, class_name, info =
55
+ if filter.is_a?(variable_mapping::AddVariables) || filter.is_a?(variable_mapping::SetVariable)
56
+ # TODO: grab user_filter here if needed for understanding
57
+ # _info = filter.instance_variable_get(:@user_filter).inspect # we could even grab the source code for callables here!
58
+ _info = ""
59
+
60
+ [id, filter.class.to_s.match(/VariableMapping::.+/), _info]
61
+ else # generic VariableMapping::DSL step such as {VariableMapping.scope}
62
+ _name = filter.inspect.match(/VariableMapping\.\w+/)
63
+
64
+ [id.to_s, _name, ""]
65
+ end
66
+
67
+ text = "#{id.ljust(45, ".")} #{info.ljust(45, ".")} #{class_name}"
68
+
69
+ [level+1, text]
70
+ end
71
+
72
+ # pp filters
73
+ render_task_wrap_step(row, level) + filters
74
+ # render_task_wrap_step(row, level)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,68 @@
1
+ module Trailblazer
2
+ module Developer
3
+ module Trace
4
+ module Debugger
5
+ # @private
6
+ # Public entry point to add Debugger::Node normalizer steps.
7
+ def self.add_normalizer_step!(step, id:, normalizer: Normalizer::PIPELINES.last, **options)
8
+ task = Normalizer.Task(step)
9
+
10
+ # We have a TaskWrap::Pipeline (a very simple style of "activity" used for normalizers) and
11
+ # add another step using the "friendly interface" from {Activity::Adds}.
12
+ options = {append: nil} unless options.any?
13
+
14
+ pipeline_extension = Activity::TaskWrap::Extension.build([task, id: id, **options])
15
+
16
+ Normalizer::PIPELINES << pipeline_extension.(normalizer)
17
+ end
18
+
19
+ module Normalizer
20
+ def self.Task(user_step) # TODO: we could keep this in the {activity} gem.
21
+ Activity::TaskWrap::Pipeline::TaskAdapter.for_step(user_step, option: false) # we don't need Option as we don't have ciruit_options here, and no {:exec_context}
22
+ end
23
+
24
+ # Default steps for the Debugger::Node options pipeline, following the step-interface.
25
+ module Default
26
+ def self.compile_id(ctx, activity:, task:, **)
27
+ ctx[:compile_id] = Activity::Introspect.Nodes(activity, task: task)[:id]
28
+ end
29
+
30
+ def self.compile_path(ctx, parent_map:, captured_node:, **)
31
+ ctx[:compile_path] = Trace::Tree::ParentMap.path_for(parent_map, captured_node)
32
+ end
33
+
34
+ def self.runtime_id(ctx, compile_id:, **)
35
+ ctx[:runtime_id] = compile_id
36
+ end
37
+
38
+ def self.runtime_path(ctx, runtime_id:, compile_path:, **)
39
+ return ctx[:runtime_path] = compile_path if compile_path.empty? # FIXME: this currently only applies to root.
40
+
41
+ ctx[:runtime_path] = compile_path[0..-2] + [runtime_id]
42
+ end
43
+
44
+ def self.label(ctx, label: nil, runtime_id:, **)
45
+ ctx[:label] = label || runtime_id
46
+ end
47
+
48
+ def self.data(ctx, data: {}, **)
49
+ ctx[:data] = data
50
+ end
51
+ end
52
+
53
+ default_steps = {
54
+ compile_id: Normalizer.Task(Default.method(:compile_id)),
55
+ compile_path: Normalizer.Task(Default.method(:compile_path)),
56
+ runtime_id: Normalizer.Task(Default.method(:runtime_id)),
57
+ runtime_path: Normalizer.Task(Default.method(:runtime_path)),
58
+ label: Normalizer.Task(Default.method(:label)),
59
+ data: Normalizer.Task(Default.method(:data)),
60
+ }.
61
+ collect { |id, task| Activity::TaskWrap::Pipeline.Row(id, task) }
62
+
63
+ PIPELINES = [Activity::TaskWrap::Pipeline.new(default_steps)] # we do mutate this constant at compile-time. Maybe # DISCUSS and find a better way.
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,72 @@
1
+ module Trailblazer
2
+ module Developer
3
+ module Trace
4
+ module Debugger
5
+ class Node < Struct.new(:captured_node, :task, :activity, :compile_id, :compile_path, :runtime_id, :runtime_path, :label, :data, :captured_input, :captured_output, :level, keyword_init: true)
6
+ # The idea is to only work with {Activity} instances on this level, as that's the runtime concept.
7
+
8
+ # TODO: class, "type",
9
+ # which track, return signal, etc
10
+
11
+
12
+ # we always key options for specific nodes by Stack::Captured::Input, so we don't confuse activities if they were called multiple times.
13
+ def self.build(tree, enumerable_tree, node_options: {}, normalizer: Debugger::Normalizer::PIPELINES.last, **options_for_nodes)
14
+ parent_map = Trace::Tree::ParentMap.build(tree).to_h # DISCUSS: can we use {enumerable_tree} for {ParentMap}?
15
+
16
+
17
+ container_activity = enumerable_tree[0].captured_input.activity # TODO: any other way to grab the container_activity? Maybe via {activity.container_activity}?
18
+
19
+ top_activity = enumerable_tree[0].captured_input.task
20
+
21
+ # DISCUSS: this might change if we introduce a new Node type for Trace.
22
+ debugger_nodes = enumerable_tree[0..-1].collect do |node|
23
+ activity = node.captured_input.activity
24
+ task = node.captured_input.task
25
+ # it's possible to pass per-node options, like {label: "Yo!"} via {:node_options[<captured_input>]}
26
+ options = node_options[node.captured_input] || {}
27
+
28
+
29
+ options_for_debugger_node, _ = normalizer.(
30
+ {
31
+ captured_node: node,
32
+ task: task,
33
+ activity: activity,
34
+ parent_map: parent_map,
35
+ **options
36
+ },
37
+ []
38
+ )
39
+
40
+ options_for_debugger_node = options_for_debugger_node.slice(*(options_for_debugger_node.keys - [:parent_map]))
41
+
42
+ # these attributes are not changing with the presentation
43
+ Debugger::Node.new(
44
+ captured_node: node,
45
+ activity: activity,
46
+ task: task,
47
+
48
+ level: node.level,
49
+ captured_input: node.captured_input,
50
+ captured_output: node.captured_output,
51
+
52
+ **options_for_debugger_node,
53
+ ).freeze
54
+ end
55
+ end
56
+
57
+ def self.build_for_stack(stack, **options_for_debugger_nodes)
58
+ tree, processed = Trace.Tree(stack.to_a)
59
+
60
+ enumerable_tree = Trace::Tree.Enumerable(tree)
61
+
62
+ Debugger::Node.build(
63
+ tree,
64
+ enumerable_tree,
65
+ **options_for_debugger_nodes,
66
+ )
67
+ end
68
+ end
69
+ end # Debugger
70
+ end # Trace
71
+ end
72
+ end
@@ -1,63 +1,45 @@
1
- require 'hirb'
1
+ require "hirb"
2
2
 
3
3
  module Trailblazer::Developer
4
4
  module Trace
5
5
  module Present
6
6
  module_function
7
7
 
8
- def default_renderer(task_node:, **)
9
- [ task_node.level, %{#{task_node.value}} ]
8
+ # @private
9
+ def default_renderer(debugger_node:, **) # DISCUSS: for compatibility, should we pass {:task_node} here, too?
10
+ [debugger_node.level, debugger_node.label]
10
11
  end
11
12
 
12
- def call(stack, level: 1, tree: [], renderer: method(:default_renderer), **options)
13
- tree(stack.to_a, level, tree: tree, renderer: renderer, **options)
14
- end
15
-
16
- def tree(stack, level, tree:, renderer:, **options)
17
- opts = options.merge(tree: tree)
18
- tree_for(stack, level, **opts)
19
-
20
- nodes = tree.each_with_index.map do |task_node, position|
21
- renderer.(task_node: task_node, position: position, tree: tree)
13
+ # Returns the console output string.
14
+ # @private
15
+ def render(debugger_nodes, renderer: method(:default_renderer), **options_for_renderer)
16
+ nodes = debugger_nodes.collect do |debugger_node|
17
+ renderer.(debugger_node: debugger_node, tree: debugger_nodes, **options_for_renderer)
22
18
  end
23
19
 
24
20
  Hirb::Console.format_output(nodes, class: :tree, type: :directory, multi_line_nodes: true)
25
21
  end
26
22
 
27
- def tree_for(stack, level, tree:, **options)
28
- stack.each do |lvl| # always a Stack::Task[input, ..., output]
29
- input, output, nested = Trace::Level.input_output_nested_for_level(lvl)
23
+ # Entry point for rendering a Stack as a "tree branch" the way we do it in {#wtf?}.
24
+ def call(stack, render_method: method(:render), node_options: {}, **options)
25
+ # The top activity doesn't have an ID, hence we need to compute a default label.
26
+ # TODO: maybe we should deep-merge here.
27
+ captured_input_for_top_activity = stack.to_a[0]
30
28
 
31
- tree.push(*TreeNodes.for(level, **options.merge(input: input, output: output)))
29
+ top_activity_options = {
30
+ # we can pass particular label "hints".
31
+ captured_input_for_top_activity => {
32
+ # label: %{#{captured_input_for_top_activity.task.superclass} (anonymous)},
33
+ label: captured_input_for_top_activity.task.inspect,
34
+ },
35
+ }
32
36
 
33
- if nested.any? # nesting
34
- opts = options.merge(tree: tree)
35
- tree_for(nested, level + 1, **opts)
36
- end
37
+ node_options = top_activity_options.merge(node_options)
37
38
 
38
- tree
39
- end
40
- end
39
+ debugger_nodes = Debugger::Node.build_for_stack(stack, node_options: node_options, **options) # currently, we agree on using a Debugger::Node list as the presentation data structure.
41
40
 
42
- module TreeNodes
43
- Node = Struct.new(:level, :value, :input, :output, :options) do
44
- # Allow access to any custom key from options, eg. color_map
45
- def method_missing(name, *)
46
- options[name]
47
- end
48
- end
49
-
50
- module_function
51
-
52
- def for(level, input:, output:, **options)
53
- nodes = Array[ Node.new(level, input.data[:task_name], input, output, options).freeze ]
54
-
55
- focused_nodes = Trace::Focusable.tree_nodes_for(level, input: input, output: output, **options)
56
- nodes += focused_nodes if focused_nodes.length > 0
57
-
58
- nodes
59
- end
41
+ return render_method.(debugger_nodes, **options)
60
42
  end
61
- end
43
+ end # Present
62
44
  end
63
45
  end
@@ -0,0 +1,21 @@
1
+ module Trailblazer
2
+ module Developer
3
+ module Trace
4
+ # The stack is a linear one-dimensional array. Per traced task two {Trace::Captured}
5
+ # elements get pushed onto it (unless there's an Exception).
6
+ class Stack
7
+ def initialize(captureds=[])
8
+ @stack = captureds
9
+ end
10
+
11
+ def <<(captured)
12
+ @stack << captured
13
+ end
14
+
15
+ def to_a
16
+ @stack
17
+ end
18
+ end # Stack
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,86 @@
1
+ module Trailblazer
2
+ module Developer
3
+ module Trace
4
+ # Datastructure representing a trace.
5
+ class Tree
6
+ # This could also be seen as {tree.to_a}.
7
+ def self.Enumerable(node)
8
+ Enumerable.nodes_for(node)
9
+ end
10
+
11
+ module Enumerable
12
+ # @private
13
+ def self.nodes_for(node)
14
+ [node, *node.nodes.collect { |n| nodes_for(n) } ].flatten
15
+ end
16
+ end # Enumerable
17
+
18
+ # Map each {Node} instance to its parent {Node}.
19
+ module ParentMap
20
+ def self.build(node)
21
+ children_map = []
22
+ node.nodes.each { |n| children_map += ParentMap.build(n) }#.flatten(1)
23
+
24
+ node.nodes.collect { |n| [n, node] } + children_map
25
+ end
26
+
27
+ # @public
28
+ def self.path_for(parent_map, node)
29
+ path = []
30
+
31
+ while parent = parent_map[node] # DISCUSS: what if the graphs are cached and present, already?
32
+ node_id = Activity::Introspect.Nodes(node.captured_input.activity, task: node.captured_input.task).id
33
+ path << node_id
34
+
35
+ node = parent
36
+ end
37
+
38
+ path.reverse
39
+ end
40
+ end
41
+
42
+ class Node < Struct.new(:level, :captured_input, :captured_output, :nodes)
43
+ end
44
+ end # Tree
45
+
46
+
47
+ # Builds a tree graph from a linear stack.
48
+ # Consists of {Tree::Node} structures.
49
+ def self.Tree(stack_end, level: 0, parent: nil)
50
+ processed = []
51
+ nodes = []
52
+
53
+ # for {captured_input} we're gonna build a {Node}!
54
+ captured_input, remaining = stack_end[0], stack_end[1..-1]
55
+
56
+ raise unless captured_input.is_a?(Captured::Input)
57
+
58
+ while next_captured = remaining[0]
59
+ if next_captured.is_a?(Captured::Input)
60
+
61
+ bla, _processed = Tree(remaining, level: level+1)
62
+ nodes += [bla]
63
+ processed += _processed
64
+
65
+
66
+ remaining = remaining - processed
67
+
68
+ else # Captured::Output
69
+
70
+ raise unless next_captured.is_a?(Captured::Output)
71
+ raise if next_captured.activity != captured_input.activity
72
+
73
+ node = Tree::Node.new(level, captured_input, next_captured, nodes)
74
+
75
+ return node,
76
+ [captured_input, *processed, next_captured] # what nodes did we process here?
77
+
78
+ end
79
+
80
+ end
81
+
82
+
83
+ end # Tree
84
+ end
85
+ end # Developer
86
+ end