Skip to content

Commit

Permalink
add suspend_task_position to Iteration.
Browse files Browse the repository at this point in the history
states are now keyed by their suspend IDS and not just by catch events.
  • Loading branch information
apotonick committed Mar 26, 2024
1 parent f5134de commit 0df5727
Show file tree
Hide file tree
Showing 9 changed files with 1,326 additions and 270 deletions.
1 change: 1 addition & 0 deletions lib/trailblazer/workflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Workflow
require "trailblazer/workflow/event"

require "trailblazer/workflow/test/plan"
require "trailblazer/workflow/test/plan/introspect"
require "trailblazer/workflow/test/assertions"

require "trailblazer/workflow/discovery"
Expand Down
12 changes: 10 additions & 2 deletions lib/trailblazer/workflow/introspect/iteration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,34 @@ module Introspect
#
# DISCUSS: maybe the naming/namespace will change.
# maybe Introspect::Iteration?
class Iteration < Struct.new(:id, :event_label, :start_task_position, :start_positions, :suspend_positions, :outcome)
class Iteration < Struct.new(:id, :event_label, :start_task_position, :start_positions, :suspend_positions, :suspend_task_position, :outcome)
class Set
def self.from_discovered_states(discovered_states, **options)
iterations = discovered_states.collect do |row|
triggered_catch_event_position = row[:positions_before][1]

id = Introspect::Present.id_for_position(triggered_catch_event_position)
event_label = Test::Plan::CommentHeader.start_position_label(row[:positions_before][1], row, **options)
event_label = Test::Plan::Introspect.start_position_label(row[:positions_before][1], row, **options)

Iteration.new(
id,
event_label,
row[:positions_before][1],
row[:positions_before][0],
row[:suspend_configuration].lane_positions,
row[:suspend_configuration].signal,
row[:outcome],
)
end

Set.new(iterations, **options)
end

def self.from_file(filename, **options) # TODO: test me, and call me from some Setup code.
serialized_iteration_set = File.read(filename)
Deserialize.(JSON.parse(serialized_iteration_set), **options)
end

def initialize(iterations, lanes_cfg:)
@iterations = iterations
@lanes_cfg = lanes_cfg
Expand Down Expand Up @@ -58,6 +64,7 @@ def call(iterations, **options)
start_task_position: Serialize.serialize_position(*iteration.start_task_position.to_a, **options),
start_positions: Serialize.serialize_suspend_positions(iteration.start_positions, **options),
suspend_positions: Serialize.serialize_suspend_positions(iteration.suspend_positions, **options),
suspend_task_position: Serialize.serialize_position(*iteration.start_task_position.to_a, **options),
outcome: iteration.outcome,
}
end
Expand Down Expand Up @@ -119,6 +126,7 @@ def call(structure, lanes_cfg:)
position_from_tuple(*attributes["start_task_position"]["tuple"], label_2_activity: label_2_activity),
positions_from(attributes["start_positions"], label_2_activity: label_2_activity),
positions_from(attributes["suspend_positions"], label_2_activity: label_2_activity),
position_from_tuple(*attributes["suspend_task_position"]["tuple"], label_2_activity: label_2_activity),
attributes["outcome"].to_sym,
)
end
Expand Down
25 changes: 14 additions & 11 deletions lib/trailblazer/workflow/introspect/state_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ def aggregate_by_state(ctx, iteration_set:, lanes_cfg:, **)
start_positions = iteration.start_positions
start_task_position = iteration.start_task_position

events = states[start_positions]
events = [] if events.nil?
events = states[start_positions] || []

events += [start_task_position]

Expand All @@ -35,20 +34,24 @@ def render_data(ctx, states:, lanes_cfg:, **)
suggested_state_names = {}

cli_rows = states.flat_map do |positions, catch_events|
suspend_tuples = positions.to_a.collect do |position|
Iteration::Set::Serialize.id_tuple_for(*position.to_a, lanes_cfg: lanes_cfg)
# suspend_tuples = positions.to_a.collect do |position|
# Iteration::Set::Serialize.id_tuple_for(*position.to_a, lanes_cfg: lanes_cfg)
# end
suspend_id_hints = positions.to_a.collect do |position| # DISCUSS: are these positions ordered lifecycle,UI,reviewer, always?
[Introspect::Present.lane_label_for(*position.to_a, lanes_cfg: lanes_cfg), Test::Plan::Introspect.id_hint_for_suspend_position(*position.to_a)].join(" ")
end

readable_suspend_id_hints = suspend_id_hints.join(" ")

suggested_state_name = suggested_state_name_for(catch_events)

suggested_state_name = "⏸︎ #{suggested_state_name}"

# add an "ID hint" to the state name (part of the actual suspend gw's ID).
if suggested_state_names[suggested_state_name]
last_lane = catch_events[0].activity # DISCUSS: could be more explicit.
suspend_id_hint = positions[last_lane].to_h[:semantic][1][-8..]

suggested_state_name = "#{suggested_state_name} (#{suspend_id_hint})"
suggested_state_name = "#{suggested_state_name}"
else
suggested_state_names[suggested_state_name] = true
end
Expand All @@ -57,9 +60,6 @@ def render_data(ctx, states:, lanes_cfg:, **)
.collect { |event_position| Present.readable_name_for_catch_event(*event_position.to_a, lanes_cfg: lanes_cfg).inspect }
.join(", ")

# key: row[:catch_events].collect { |position| Introspect::Present.id_for_position(position) }.uniq.sort



Hash[
"state name",
Expand All @@ -68,6 +68,9 @@ def render_data(ctx, states:, lanes_cfg:, **)
"triggerable events",
triggerable_events,

"Suspend IDs",
readable_suspend_id_hints,

:positions,
positions,

Expand All @@ -88,14 +91,14 @@ def suggested_state_name_for(catch_events)
.join("♦")
end

# Render the actual table, for CLI.
# Render the actual table, for CLI and debugging.
class Render < Trailblazer::Activity::Railway
step Subprocess(StateTable)
step :render
def render(ctx, rows:, **)
cli_rows = rows.collect { |row| row.merge("state name" => row["state name"].inspect) }

columns = ["state name", "triggerable events"]
columns = ["state name", "triggerable events", "Suspend IDs"]
ctx[:table] = Present::Table.render(columns, cli_rows)
end
end
Expand Down
102 changes: 0 additions & 102 deletions lib/trailblazer/workflow/test/plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,108 +17,6 @@ def for(iteration_set, input:, **options)
end
end

def render_comment_header(iteration_set, **options)
CommentHeader.(iteration_set, **options)
end

module CommentHeader
module_function

def call(iteration_set, **options)
start_position_combined_column = render_combined_column_labels(iteration_set.collect { |iteration| iteration.start_positions }, **options)
expected_position_combined_column = render_combined_column_labels(iteration_set.collect { |iteration| iteration.suspend_positions }, **options)

rows = iteration_set.collect.with_index do |iteration, index|
Hash[
"triggered catch",
start_position_label(iteration.start_task_position, iteration, **options),

"start configuration",
start_position_combined_column[index],

"expected reached configuration",
expected_position_combined_column[index],
]
end

Introspect::Present::Table.render(["triggered catch", "start configuration", "expected reached configuration"], rows)
end

def start_position_label(start_position, row, **options)
outcome = row[:outcome]

start_position_label_for(start_position, expected_outcome: outcome, **options)
end

def start_position_label_for(position, expected_outcome:, **options)
event_label = Introspect::Present.readable_name_for_catch_event(*position.to_a, **options)

event_label += " #{Introspect::Present::ICONS[:failure]}" if expected_outcome == :failure # FIXME: what happens to :symbol after serialization?

event_label
end

# Render the content of a combined column (usually used for positions, such as {}).
# Note that this renders for the entire table/all rows.
#
# ⛾ ⏵︎Update ⏵︎Notify approver ☝ ⏵︎Update form ⏵︎Notify approver ☑ ⏵︎xxx
def render_combined_column_labels(positions_rows, **options)
all_position_labels = positions_rows.collect do |positions|
positions.collect do |activity, task|
[
activity,
Introspect::Present.readable_name_for_suspend_or_terminus(activity, task, **options)
]
end
end

position_combined_column = format_positions_column(all_position_labels, **options)
end

def compute_combined_column_widths(position_rows, lanes_cfg:, **)
chars_to_filter = Introspect::Present::ICONS.values + lanes_cfg.to_h.collect { |_, cfg| cfg[:icon] } # TODO: do this way up in the code path.

# Find out the longest entry per lane.
columns = lanes_cfg.to_h.collect { |_, cfg| [cfg[:activity], []] }.to_h # {<lifecycle> => [], ...}

position_rows.each do |event_labels|
event_labels.each do |(activity, suspend_label)|
length = suspend_label ? label_length(suspend_label, chars_to_filter: chars_to_filter) : 0 # DISCUSS: why can {suspend_label} be nil?

columns[activity] << length
end
end

_columns_2_length = columns.collect { |activity, lengths| [activity, lengths.max] }.to_h
end

# @private
def label_length(label, chars_to_filter:)
countable_label = chars_to_filter.inject(label) { |memo, char| memo.gsub(char, "@") }
countable_label.length
end

def format_positions_column(position_rows, lanes_cfg:, **options)
columns_2_length = compute_combined_column_widths(position_rows, lanes_cfg: lanes_cfg, **options)

rows = position_rows.collect do |event_labels|
columns = event_labels.collect do |activity, suspend_label|
col_length = columns_2_length[activity]

# Correct {#ljust}, it considers one emoji as two characters,
# hence we need to add those when padding.
emoji_overflows = suspend_label.chars.find_all { |char| char == "︎" } # this is a "wrong" character behind an emoji.

suspend_label.ljust(col_length + emoji_overflows.size, " ")
end

content = columns.join(" ")
end

rows
end
end

end

end # Test
Expand Down
112 changes: 112 additions & 0 deletions lib/trailblazer/workflow/test/plan/introspect.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
module Trailblazer::Workflow
module Test
module Plan
# Build a Test::Plan from {Iteration::Set}.
module Introspect
module_function

def call(iteration_set, **options)
start_position_combined_column = render_combined_column_labels(iteration_set.collect { |iteration| iteration.start_positions }, **options)
expected_position_combined_column = render_combined_column_labels(iteration_set.collect { |iteration| iteration.suspend_positions }, **options)

rows = iteration_set.collect.with_index do |iteration, index|
Hash[
"triggered catch",
start_position_label(iteration.start_task_position, iteration, **options),

"start configuration",
start_position_combined_column[index],

"expected reached configuration",
expected_position_combined_column[index],
]
end

Trailblazer::Workflow::Introspect::Present::Table.render(["triggered catch", "start configuration", "expected reached configuration"], rows)
end

def start_position_label(start_position, row, **options)
outcome = row[:outcome]

start_position_label_for(start_position, expected_outcome: outcome, **options)
end

def start_position_label_for(position, expected_outcome:, **options)
event_label = Trailblazer::Workflow::Introspect::Present.readable_name_for_catch_event(*position.to_a, **options)

event_label += " #{Trailblazer::Workflow::Introspect::Present::ICONS[:failure]}" if expected_outcome == :failure # FIXME: what happens to :symbol after serialization?

event_label
end

# Render the content of a combined column (usually used for positions, such as {}).
# Note that this renders for the entire table/all rows.
#
# ⛾ ⏵︎Update ⏵︎Notify approver ☝ ⏵︎Update form ⏵︎Notify approver ☑ ⏵︎xxx
def render_combined_column_labels(positions_rows, **options)
all_position_labels = positions_rows.collect do |positions|
positions.collect do |activity, task|
[
activity,
Trailblazer::Workflow::Introspect::Present.readable_name_for_suspend_or_terminus(activity, task, **options) +
" <#{id_hint_for_suspend_position(activity, task)}>" # TODO: make this optional.
]
end
end

position_combined_column = format_positions_column(all_position_labels, **options)
end

# TODO: this is a quite pathetic way to show the native JSON ID, or part of it. We should store
# the native ID on the Generate level.
def id_hint_for_suspend_position(activity, task)
id = Trailblazer::Activity::Introspect.Nodes(activity, task: task).id # TODO: use {Introspect.id_for_position}
id_part = id[-7..-4]
end

def compute_combined_column_widths(position_rows, lanes_cfg:, **)
chars_to_filter = Trailblazer::Workflow::Introspect::Present::ICONS.values + lanes_cfg.to_h.collect { |_, cfg| cfg[:icon] } # TODO: do this way up in the code path.

# Find out the longest entry per lane.
columns = lanes_cfg.to_h.collect { |_, cfg| [cfg[:activity], []] }.to_h # {<lifecycle> => [], ...}

position_rows.each do |event_labels|
event_labels.each do |(activity, suspend_label)|
length = suspend_label ? label_length(suspend_label, chars_to_filter: chars_to_filter) : 0 # DISCUSS: why can {suspend_label} be nil?

columns[activity] << length
end
end

_columns_2_length = columns.collect { |activity, lengths| [activity, lengths.max] }.to_h
end

# @private
def label_length(label, chars_to_filter:)
countable_label = chars_to_filter.inject(label) { |memo, char| memo.gsub(char, "@") }
countable_label.length
end

def format_positions_column(position_rows, lanes_cfg:, **options)
columns_2_length = compute_combined_column_widths(position_rows, lanes_cfg: lanes_cfg, **options)

rows = position_rows.collect do |event_labels|
columns = event_labels.collect do |activity, suspend_label|
col_length = columns_2_length[activity]

# Correct {#ljust}, it considers one emoji as two characters,
# hence we need to add those when padding.
emoji_overflows = suspend_label.chars.find_all { |char| char == "︎" } # this is a "wrong" character behind an emoji.

suspend_label.ljust(col_length + emoji_overflows.size, " ")
end

content = columns.join(" ")
end

rows
end
end
end
end
end
Loading

0 comments on commit 0df5727

Please sign in to comment.