From af66e41c53b63089d81705a2d66b76c47047c8fd Mon Sep 17 00:00:00 2001 From: Rodrigo Serradura Date: Sat, 30 Dec 2023 10:26:46 -0300 Subject: [PATCH] [WIP] Ensure the tracking on nested and_then callings --- lib/bcdd/result/transitions.rb | 9 +- lib/bcdd/result/transitions/tracking.rb | 5 +- .../result/transitions/tracking/disabled.rb | 4 +- .../result/transitions/tracking/enabled.rb | 76 ++++++------- lib/bcdd/result/transitions/tree.rb | 95 ++++++++++++++++ sig/bcdd/result/transitions.rbs | 67 ++++++++--- .../with_subject/singleton/nested_test.rb | 24 ++-- .../with_subject/instance/flat_test.rb | 12 +- .../with_subject/instance/nested_test.rb | 48 ++++---- .../with_subject/singleton/nested_test.rb | 106 ++++++++++++------ test/test_helper.rb | 21 +++- 11 files changed, 318 insertions(+), 149 deletions(-) create mode 100644 lib/bcdd/result/transitions/tree.rb diff --git a/lib/bcdd/result/transitions.rb b/lib/bcdd/result/transitions.rb index 395847b..2fcef1b 100644 --- a/lib/bcdd/result/transitions.rb +++ b/lib/bcdd/result/transitions.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'securerandom' - class BCDD::Result module Transitions + require_relative 'transitions/tree' require_relative 'transitions/tracking' THREAD_VAR_NAME = :bcdd_result_transitions_tracking @@ -13,14 +12,14 @@ def self.tracking end end - def self.transitions(id: SecureRandom.uuid, name: nil, desc: nil) - Transitions.tracking.start(id: id, name: name, desc: desc) + def self.transitions(name: nil, desc: nil) + Transitions.tracking.start(name: name, desc: desc) result = yield result.is_a?(::BCDD::Result) or raise Error::UnexpectedOutcome.build(outcome: result, origin: :transitions) - Transitions.tracking.finish(id: id, result: result) + Transitions.tracking.finish(result: result) result rescue ::Exception => e diff --git a/lib/bcdd/result/transitions/tracking.rb b/lib/bcdd/result/transitions/tracking.rb index 512861b..c95ddbd 100644 --- a/lib/bcdd/result/transitions/tracking.rb +++ b/lib/bcdd/result/transitions/tracking.rb @@ -6,10 +6,11 @@ module Tracking require_relative 'tracking/enabled' require_relative 'tracking/disabled' - EMPTY_HASH = {}.freeze EMPTY_ARRAY = [].freeze + EMPTY_HASH = {}.freeze + EMPTY_TREE = Tree.new(nil).freeze VERSION = 1 - EMPTY = { records: EMPTY_ARRAY, version: VERSION }.freeze + EMPTY = { version: VERSION, records: EMPTY_ARRAY, metadata: { duration: 0, tree_map: EMPTY_ARRAY } }.freeze def self.instance Config.instance.feature.enabled?(:transitions) ? Tracking::Enabled.new : Tracking::Disabled diff --git a/lib/bcdd/result/transitions/tracking/disabled.rb b/lib/bcdd/result/transitions/tracking/disabled.rb index df8f8d2..7fa8b7a 100644 --- a/lib/bcdd/result/transitions/tracking/disabled.rb +++ b/lib/bcdd/result/transitions/tracking/disabled.rb @@ -2,9 +2,9 @@ module BCDD::Result::Transitions module Tracking::Disabled - def self.start(id:, name:, desc:); end + def self.start(name:, desc:); end - def self.finish(id:, result:); end + def self.finish(result:); end def self.reset!; end diff --git a/lib/bcdd/result/transitions/tracking/enabled.rb b/lib/bcdd/result/transitions/tracking/enabled.rb index 54a5efd..b5b1ad2 100644 --- a/lib/bcdd/result/transitions/tracking/enabled.rb +++ b/lib/bcdd/result/transitions/tracking/enabled.rb @@ -2,43 +2,38 @@ module BCDD::Result::Transitions class Tracking::Enabled - attr_accessor :root, :parent, :current, :parents, :records, :current_and_then + attr_accessor :tree, :records, :root_started_at - private :root, :root=, :parent, :parent=, :current, :current= - private :parents, :parents=, :records, :records=, :current_and_then, :current_and_then= + private :tree, :tree=, :records, :records=, :root_started_at, :root_started_at= - def start(id:, name:, desc:) - root.frozen? and return root_start(id, name, desc) + def start(name:, desc:) + name_and_desc = [name, desc] - self.parent = current if parent[:id] != current[:id] - self.current = { id: id, name: name, desc: desc } - - parents[id] = parent + tree.frozen? ? root_start(name_and_desc) : tree.insert!(name_and_desc) end - def finish(id:, result:) - self.current = parents[id] - self.parent = parents.fetch(current[:id]) + def finish(result:) + node = tree.current + + tree.move_up! + + return unless node.root? - return if root && root[:id] != id + duration = (now_in_milliseconds - root_started_at) - result.send(:transitions=, records: records, version: Tracking::VERSION) + metadata = { duration: duration, tree_map: tree.nested_ids } + + result.send(:transitions=, version: Tracking::VERSION, records: records, metadata: metadata) reset! end def reset! - self.root = Tracking::EMPTY_HASH - self.parent = Tracking::EMPTY_HASH - self.current = Tracking::EMPTY_HASH - self.parents = Tracking::EMPTY_HASH - self.records = Tracking::EMPTY_ARRAY - - reset_current_and_then! + self.tree = Tracking::EMPTY_TREE end def record(result) - return if root.frozen? + return if tree.frozen? track(result, time: ::Time.now.getutc) end @@ -46,43 +41,40 @@ def record(result) def record_and_then(type_arg, arg, subject) type = type_arg.instance_of?(::Method) ? :method : type_arg - self.current_and_then = { type: type, arg: arg, subject: subject } + unless tree.frozen? + current_and_then = { type: type, arg: arg, subject: subject } + current_and_then[:method_name] = type_arg.name if type == :method - current_and_then[:method_name] = type_arg.name if type == :method + tree.current.value[1] = current_and_then + end - result = yield - - reset_current_and_then! - - result + yield end private - def root_start(id, name, desc) - self.current = { id: id, name: name, desc: desc } - self.parent = current - self.root = current + TreeNodeValueNormalizer = ->(id, (nam, des)) { [{ id: id, name: nam, desc: des }, Tracking::EMPTY_HASH] } + + def root_start(name_and_desc) + self.root_started_at = now_in_milliseconds - self.parents = { current[:id] => current } self.records = [] - reset_current_and_then! + self.tree = Tree.new(name_and_desc, normalizer: TreeNodeValueNormalizer) end def track(result, time:) result = result.data.to_h - and_then = current_and_then - - record = - { root: root, parent: parent, current: current, result: result, and_then: and_then, time: time } + root, = tree.root_value + parent, = tree.parent_value + current, and_then = tree.current_value - records << record + records << { root: root, parent: parent, current: current, result: result, and_then: and_then, time: time } end - def reset_current_and_then! - self.current_and_then = Tracking::EMPTY_HASH + def now_in_milliseconds + Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) end end end diff --git a/lib/bcdd/result/transitions/tree.rb b/lib/bcdd/result/transitions/tree.rb new file mode 100644 index 0000000..dfc67e6 --- /dev/null +++ b/lib/bcdd/result/transitions/tree.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +class BCDD::Result + module Transitions + class Tree + class Node + attr_reader :id, :value, :parent, :normalizer, :children + + def initialize(value, parent:, id:, normalizer:) + @normalizer = normalizer + + @id = id + @value = normalizer.call(id, value) + @parent = parent + + @children = [] + end + + def insert(value, id:) + node = self.class.new(value, parent: self, id: id, normalizer: normalizer) + + @children << node + + node + end + + def root? + parent.nil? + end + + def leaf? + children.empty? + end + + def node? + !leaf? + end + + def inspect + "#<#{self.class.name} id=#{id} children.size=#{children.size}>" + end + end + + attr_reader :size, :root, :current + + def initialize(value, normalizer: ->(_id, val) { val }) + @size = 0 + + @root = Node.new(value, parent: nil, id: @size, normalizer: normalizer) + + @current = @root + end + + def root_value + root.value + end + + def parent_value + current.parent&.value || root_value + end + + def current_value + current.value + end + + def insert(value) + @size += 1 + + current.insert(value, id: size) + end + + def insert!(value) + @current = insert(value) + end + + def move_up!(level = 1) + tap { level.times { @current = current.parent || root } } + end + + def move_down!(level = 1, index: -1) + tap { level.times { current.children[index].then { |child| @current = child if child } } } + end + + def move_to_root! + tap { @current = root } + end + + NestedIds = ->(node) { [node.id, node.children.map(&NestedIds)] } + + def nested_ids + NestedIds[root] + end + end + end +end diff --git a/sig/bcdd/result/transitions.rbs b/sig/bcdd/result/transitions.rbs index 6b935be..372646b 100644 --- a/sig/bcdd/result/transitions.rbs +++ b/sig/bcdd/result/transitions.rbs @@ -1,38 +1,79 @@ class BCDD::Result module Transitions + class Tree + class Node + attr_reader id: Integer + attr_reader value: untyped + attr_reader parent: (Node | nil) + attr_reader normalizer: ^(Integer, Array[untyped]) -> untyped + attr_reader children: Array[Node] + + def initialize: ( + untyped value, + parent: (Node | nil), + id: Integer, + normalizer: ^(Integer, Array[untyped]) -> untyped + ) -> void + + def insert: (untyped, id: Integer) -> Node + + def root?: () -> bool + def leaf?: () -> bool + def node?: () -> bool + def inspect: () -> String + end + + attr_reader size: Integer + attr_reader root: Node + attr_reader current: Node + + def initialize: (untyped, ?normalizer: ^(Integer, Array[untyped]) -> untyped) -> void + def root_value: () -> untyped + def parent_value: () -> untyped + def current_value: () -> untyped + def insert: (untyped) -> Node + def insert!: (untyped) -> Node + def move_up!: (?Integer level) -> Tree + def move_down!: (?Integer level) -> Tree + def move_to_root!: () -> Tree + + NestedIds: ^(Node) -> Array[untyped] + + def nested_ids: () -> Array[untyped] + end + module Tracking - EMPTY_HASH: Hash[untyped, untyped] EMPTY_ARRAY: Array[untyped] + EMPTY_HASH: Hash[untyped, untyped] + EMPTY_TREE: Transitions::Tree VERSION: Integer EMPTY: Hash[Symbol, untyped] - class Enabled - private attr_accessor root: Hash[Symbol, untyped] - private attr_accessor parent: Hash[Symbol, untyped] - private attr_accessor current: Hash[Symbol, untyped] - private attr_accessor parents: Hash[String, untyped] + private attr_accessor tree: Transitions::Tree private attr_accessor records: Array[Hash[Symbol, untyped]] - private attr_accessor current_and_then: Hash[Symbol, untyped] + private attr_accessor root_started_at: Integer - def start: (id: String, name: String, desc: String) -> void - def finish: (id: String, result: BCDD::Result) -> void + def start: (name: String, desc: String) -> void + def finish: (result: BCDD::Result) -> void def reset!: () -> void def record: (BCDD::Result) -> void def record_and_then: ((untyped), untyped, untyped) { () -> BCDD::Result } -> BCDD::Result private - def root_start: (String id, String name, String desc) -> void + TreeNodeValueNormalizer: ^(Integer, Array[untyped]) -> untyped + + def root_start: (Array[untyped]) -> void def track: (BCDD::Result, time: Time) -> void - def reset_current_and_then!: () -> void + def now_in_milliseconds: () -> Integer end module Disabled - def self.start: (id: String, name: String, desc: String) -> void - def self.finish: (id: String, result: BCDD::Result) -> void + def self.start: (name: String, desc: String) -> void + def self.finish: (result: BCDD::Result) -> void def self.reset!: () -> void def self.record: (BCDD::Result) -> void def self.record_and_then: ((untyped), untyped, untyped) { () -> BCDD::Result } -> BCDD::Result diff --git a/test/bcdd/result/context/transitions/enabled/with_subject/singleton/nested_test.rb b/test/bcdd/result/context/transitions/enabled/with_subject/singleton/nested_test.rb index 0065915..bbb9331 100644 --- a/test/bcdd/result/context/transitions/enabled/with_subject/singleton/nested_test.rb +++ b/test/bcdd/result/context/transitions/enabled/with_subject/singleton/nested_test.rb @@ -119,26 +119,26 @@ def divide_by_two(num) root = last_transition.fetch(:current) - assert_hash_schema!({ id: Regexps::UUID, name: nil, desc: nil }, root) + assert_hash_schema!({ id: Integer, name: nil, desc: nil }, root) { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :failure, type: :invalid_arg, value: { message: 'num1 must be numeric' } } }.then { |spec| assert_transition_record(result, 0, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :failure, type: :invalid_arg, value: { message: 'num1 must be numeric' } } }.then { |spec| assert_transition_record(result, 1, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :failure, type: :invalid_arg, value: { message: 'num1 must be numeric' } } }.then { |spec| assert_transition_record(result, 2, spec) } @@ -165,14 +165,14 @@ def divide_by_two(num) root = last_transition.fetch(:current) - assert_hash_schema!({ id: Regexps::UUID, name: nil, desc: nil }, root) + assert_hash_schema!({ id: Integer, name: nil, desc: nil }, root) # 1st division transition # { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :failure, type: :invalid_arg, value: { message: 'num1 must be numeric' } } }.then { |spec| assert_transition_record(result, 0, spec) } @@ -186,14 +186,14 @@ def divide_by_two(num) { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :success, type: :continued, value: { num1: 20, num2: 2 } } }.then { |spec| assert_transition_record(result, 1, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :success, type: :continued, value: {} }, and_then: { type: :method, arg: { useless_arg: true }, subject: Division, method_name: :validate_nonzero } }.then { |spec| assert_transition_record(result, 2, spec) } @@ -201,7 +201,7 @@ def divide_by_two(num) { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :success, type: :division_completed, value: { number: 10 } }, and_then: { type: :method, arg: {}, subject: Division, method_name: :divide } }.then { |spec| assert_transition_record(result, 3, spec) } @@ -216,14 +216,14 @@ def divide_by_two(num) { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :success, type: :continued, value: { num1: 0, num2: 2 } } }.then { |spec| assert_transition_record(result, 4, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :success, type: :continued, value: {} }, and_then: { type: :method, arg: { useless_arg: true }, subject: Division, method_name: :validate_nonzero } }.then { |spec| assert_transition_record(result, 5, spec) } @@ -231,7 +231,7 @@ def divide_by_two(num) { root: root, parent: root, - current: { id: Regexps::UUID, name: nil, desc: nil }, + current: { id: Integer, name: nil, desc: nil }, result: { kind: :success, type: :division_completed, value: { number: 0 } }, and_then: { type: :method, arg: {}, subject: Division, method_name: :divide } }.then { |spec| assert_transition_record(result, 6, spec) } diff --git a/test/bcdd/result/transitions/enabled/with_subject/instance/flat_test.rb b/test/bcdd/result/transitions/enabled/with_subject/instance/flat_test.rb index 47164c1..9bfe0f5 100644 --- a/test/bcdd/result/transitions/enabled/with_subject/instance/flat_test.rb +++ b/test/bcdd/result/transitions/enabled/with_subject/instance/flat_test.rb @@ -181,11 +181,9 @@ def divide((num1, num2)) assert_equal(1, result5.transitions[:records].map { [_1.dig(:root, :id), _1.dig(:current, :id)] }.flatten.uniq.size) assert_equal( - 5, + [0], [result1, result2, result3, result4, result5] - .map { |result| result.transitions[:records].map { _1.dig(:root, :id) }.uniq } - .uniq - .size + .map { |result| result.transitions[:records].map { _1.dig(:root, :id) }.uniq }.uniq.flatten ) assert_equal(1, result1.transitions[:records].map { _1[:time] }.tap { assert_equal(_1.sort, _1) }.uniq.size) @@ -197,9 +195,9 @@ def divide((num1, num2)) def assert_division_transition(result, index, options) scope = { - root: { id: Regexps::UUID, name: 'Division', desc: 'divide two numbers' }, - parent: { id: Regexps::UUID, name: 'Division', desc: 'divide two numbers' }, - current: { id: Regexps::UUID, name: 'Division', desc: 'divide two numbers' } + root: { id: Integer, name: 'Division', desc: 'divide two numbers' }, + parent: { id: Integer, name: 'Division', desc: 'divide two numbers' }, + current: { id: Integer, name: 'Division', desc: 'divide two numbers' } } assert_transition_record(result, index, scope.merge(options)) diff --git a/test/bcdd/result/transitions/enabled/with_subject/instance/nested_test.rb b/test/bcdd/result/transitions/enabled/with_subject/instance/nested_test.rb index 95d7e1d..50aae29 100644 --- a/test/bcdd/result/transitions/enabled/with_subject/instance/nested_test.rb +++ b/test/bcdd/result/transitions/enabled/with_subject/instance/nested_test.rb @@ -132,26 +132,26 @@ def divide_by_two(num) root = last_transition.fetch(:current) - assert_hash_schema!({ id: Regexps::UUID, name: 'SumDivisionsByTwo', desc: nil }, root) + assert_hash_schema!({ id: Integer, name: 'SumDivisionsByTwo', desc: nil }, root) { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :failure, type: :invalid_arg, value: 'num1 must be numeric' } }.then { |spec| assert_transition_record(result, 0, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :failure, type: :invalid_arg, value: 'num1 must be numeric' } }.then { |spec| assert_transition_record(result, 1, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :failure, type: :invalid_arg, value: 'num1 must be numeric' } }.then { |spec| assert_transition_record(result, 2, spec) } @@ -178,14 +178,14 @@ def divide_by_two(num) root = last_transition.fetch(:current) - assert_hash_schema!({ id: Regexps::UUID, name: 'SumDivisionsByTwo', desc: nil }, root) + assert_hash_schema!({ id: Integer, name: 'SumDivisionsByTwo', desc: nil }, root) # 1st division transition # { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :failure, type: :invalid_arg, value: 'num1 must be numeric' } }.then { |spec| assert_transition_record(result, 0, spec) } @@ -199,30 +199,28 @@ def divide_by_two(num) { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :success, type: :ok, value: [20, 2] } }.then { |spec| assert_transition_record(result, 1, spec) } { root: root, - parent: { id: Regexps::UUID, name: 'CheckForZeros', desc: nil }, - current: { id: Regexps::UUID, name: 'DetectZero', desc: nil }, - result: { kind: :failure, type: :not_zero, value: nil }, - and_then: { type: :method, arg: 'useless_arg', subject: -> { _1.is_a?(Division) }, method_name: :divide } + parent: { id: Integer, name: 'CheckForZeros', desc: nil }, + current: { id: Integer, name: 'DetectZero', desc: nil }, + result: { kind: :failure, type: :not_zero, value: nil } }.then { |spec| assert_transition_record(result, 2, spec) } { root: root, - parent: { id: Regexps::UUID, name: 'Division', desc: nil }, - current: { id: Regexps::UUID, name: 'CheckForZeros', desc: nil }, - result: { kind: :failure, type: :no_zeros, value: nil }, - and_then: { type: :method, arg: 'useless_arg', subject: -> { _1.is_a?(Division) }, method_name: :divide } + parent: { id: Integer, name: 'Division', desc: nil }, + current: { id: Integer, name: 'CheckForZeros', desc: nil }, + result: { kind: :failure, type: :no_zeros, value: nil } }.then { |spec| assert_transition_record(result, 3, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :success, type: :division_completed, value: 10 }, and_then: { type: :method, arg: 'useless_arg', subject: -> { _1.is_a?(Division) }, method_name: :divide } }.then { |spec| assert_transition_record(result, 4, spec) } @@ -237,30 +235,28 @@ def divide_by_two(num) { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :success, type: :ok, value: [0, 2] } }.then { |spec| assert_transition_record(result, 5, spec) } { root: root, - parent: { id: Regexps::UUID, name: 'CheckForZeros', desc: nil }, - current: { id: Regexps::UUID, name: 'DetectZero', desc: nil }, - result: { kind: :failure, type: :not_zero, value: nil }, - and_then: { type: :method, arg: 'useless_arg', subject: -> { _1.is_a?(Division) }, method_name: :divide } + parent: { id: Integer, name: 'CheckForZeros', desc: nil }, + current: { id: Integer, name: 'DetectZero', desc: nil }, + result: { kind: :failure, type: :not_zero, value: nil } }.then { |spec| assert_transition_record(result, 6, spec) } { root: root, - parent: { id: Regexps::UUID, name: 'Division', desc: nil }, - current: { id: Regexps::UUID, name: 'CheckForZeros', desc: nil }, - result: { kind: :success, type: :num1_is_zero, value: nil }, - and_then: { type: :method, arg: 'useless_arg', subject: -> { _1.is_a?(Division) }, method_name: :divide } + parent: { id: Integer, name: 'Division', desc: nil }, + current: { id: Integer, name: 'CheckForZeros', desc: nil }, + result: { kind: :success, type: :num1_is_zero, value: nil } }.then { |spec| assert_transition_record(result, 7, spec) } { root: root, parent: root, - current: { id: Regexps::UUID, name: 'Division', desc: nil }, + current: { id: Integer, name: 'Division', desc: nil }, result: { kind: :success, type: :division_completed, value: 0 }, and_then: { type: :method, arg: 'useless_arg', subject: -> { _1.is_a?(Division) }, method_name: :divide } }.then { |spec| assert_transition_record(result, 8, spec) } diff --git a/test/bcdd/result/transitions/enabled/with_subject/singleton/nested_test.rb b/test/bcdd/result/transitions/enabled/with_subject/singleton/nested_test.rb index 8bfcd04..e6b0143 100644 --- a/test/bcdd/result/transitions/enabled/with_subject/singleton/nested_test.rb +++ b/test/bcdd/result/transitions/enabled/with_subject/singleton/nested_test.rb @@ -5,56 +5,92 @@ class BCDD::Result::TransitionsEnabledWithSubjectSingletonNestedTest < Minitest::Test include BCDDResultTransitionAssertions + module Giveable + def Given(value) + _ResultAs(BCDD::Result::Success, :initial_input, value) + end + end + + module CheckForZeros + extend self, Giveable, BCDD::Result.mixin(config: { addon: { continue: true } }) + + def call(numbers) + BCDD::Result.transitions(name: 'CheckForZeros') do + Given(numbers) + .and_then(:detect_zero, index: 1) + .and_then(:detect_zero, index: 0) + .then { _1.success?(:continued) ? Failure(:no_zeros, numbers) : _1 } + end + end + + private + + def detect_zero(numbers, ref) + index = ref.fetch(:index) + + numbers[index].zero? ? Success(:"number#{index + 1}_is_zero", numbers) : Continue(numbers) + end + end + module Division - extend self, BCDD::Result.mixin + extend self, Giveable, BCDD::Result.mixin(config: { addon: { continue: true } }) def call(num1, num2) - BCDD::Result.transitions do - validate_numbers(num1, num2) - .and_then(:validate_nonzero) + BCDD::Result.transitions(name: 'Division') do + Given([num1, num2]) + .and_then(:validate_numbers) + .and_then(:check_for_zeros) .and_then(:divide) end end private - def validate_numbers(num1, num2) + def validate_numbers((num1, num2)) num1.is_a?(Numeric) or return Failure(:invalid_arg, 'num1 must be numeric') num2.is_a?(Numeric) or return Failure(:invalid_arg, 'num2 must be numeric') - Success(:ok, [num1, num2]) + Continue([num1, num2]) end - def validate_nonzero(numbers) - return Failure(:division_by_zero, 'num2 cannot be zero') if numbers.last.zero? - - Success(:ok, numbers) + def check_for_zeros(numbers) + CheckForZeros.call(numbers).handle do |on| + on[:no_zeros] { Continue(numbers) } + on[:number1_is_zero] { Success(:division_completed, 0) } + on[:number2_is_zero] { Failure(:division_by_zero, 'num2 cannot be zero') } + end end def divide((num1, num2)) - Success(:ok, num1 / num2) + Success(:division_completed, num1 / num2) end end module SumDivisionsByTwo - extend self, BCDD::Result.mixin + extend self, Giveable, BCDD::Result.mixin(config: { addon: { continue: true } }) def call(*numbers) - BCDD::Result.transitions do - divisions = numbers.map { divide_by_two(_1) } - - if divisions.any?(&:failure?) - Failure(:errors, divisions.select(&:failure?).map(&:value)) - else - Success(:sum, divisions.sum(&:value)) - end + BCDD::Result.transitions(name: 'SumDivisionsByTwo') do + Given(numbers) + .and_then(:divide_numbers_by_two) + .and_then(:sum_divisions) end end private - def divide_by_two(num) - Division.call(num, 2) + def divide_numbers_by_two(numbers) + divisions = numbers.map { Division.call(_1, 2) } + + Continue(divisions) + end + + def sum_divisions(divisions) + if divisions.any?(&:failure?) + Failure(:errors, divisions.select(&:failure?).map(&:value)) + else + Success(:sum, divisions.sum(&:value)) + end end end @@ -64,10 +100,12 @@ def divide_by_two(num) result3 = SumDivisionsByTwo.call(30, 20, '10') result4 = SumDivisionsByTwo.call(30, 20, 10) - assert_transitions(result1, size: 4) - assert_transitions(result2, size: 6) - assert_transitions(result3, size: 8) - assert_transitions(result4, size: 10) + assert_transitions(result1, size: 9) + assert_transitions(result2, size: 15) + assert_transitions(result3, size: 21) + assert_transitions(result4, size: 27) + + refute_empty(result1.transitions[:records][7][:and_then]) end test 'nested transitions tracking in different threads' do @@ -81,10 +119,10 @@ def divide_by_two(num) result3 = t3.value result4 = t4.value - assert_transitions(result1, size: 4) - assert_transitions(result2, size: 6) - assert_transitions(result3, size: 8) - assert_transitions(result4, size: 10) + assert_transitions(result1, size: 9) + assert_transitions(result2, size: 15) + assert_transitions(result3, size: 21) + assert_transitions(result4, size: 27) end test 'the standard error handling' do @@ -95,8 +133,8 @@ def divide_by_two(num) result1 = SumDivisionsByTwo.call(30, 20, '10') result2 = SumDivisionsByTwo.call(30, 20, 10) - assert_transitions(result1, size: 8) - assert_transitions(result2, size: 10) + assert_transitions(result1, size: 21) + assert_transitions(result2, size: 27) end test 'an exception error handling' do @@ -107,7 +145,7 @@ def divide_by_two(num) result1 = SumDivisionsByTwo.call(30, 20, 10) result2 = SumDivisionsByTwo.call(30, 20, '10') - assert_transitions(result1, size: 10) - assert_transitions(result2, size: 8) + assert_transitions(result1, size: 27) + assert_transitions(result2, size: 21) end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1b32143..649c447 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -33,10 +33,6 @@ def self.test(name, &block) end end -module Regexps - UUID = /\A[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\z/.freeze -end - module HashSchemaAssertions def assert_hash_schema!(spec, hash, ignore_keys: []) keys_to_compare = hash.keys - ignore_keys @@ -103,10 +99,23 @@ def assert_empty_transitions(result, version: 1) def assert_transitions(result, size:, version: 1) assert_instance_of(Hash, result.transitions) - assert_equal(%i[records version], result.transitions.keys.sort) + assert_equal(%i[metadata records version], result.transitions.keys.sort) - assert_equal(size, result.transitions[:records].size) assert_equal(version, result.transitions[:version]) + assert_equal(size, result.transitions[:records].size) + + assert_transitions_metadata(result) + end + + def assert_transitions_metadata(result) + assert_instance_of(Hash, result.transitions[:metadata]) + + metadata = result.transitions[:metadata] + + assert_equal(%i[duration tree_map], metadata.keys.sort) + + assert_instance_of(Integer, metadata[:duration]) + assert_instance_of(Array, metadata[:tree_map]) end def assert_transition_record(result, index, options)