Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Surgical Refactors

Justin Searls
September 09, 2016

Surgical Refactors

As presented on September 9th at RubyKaigi 2016 in Kyoto, Japan.
Video here: http://blog.testdouble.com/posts/2016-09-16-surgical-refactors-with-suture

Justin Searls

September 09, 2016
Tweet

More Decks by Justin Searls

Other Decks in Programming

Transcript

  1. 4 3

  2. 3 3

  3. Refactor - verb To change the design of code without

    changing its observable behavior.
  4. Refactor - verb To change in advance of a new

    feature or bug fix, making the job easier.
  5. 1. Scare them! "If we don't refactor, then . !"

    to rewrite everything someday we'll need
  6. 1. Scare them! "If we don't refactor, then . !"

    to rewrite everything someday we'll need Far in the future
  7. 1. Scare them! "If we don't refactor, then . !"

    costs will be much higher your maintenance
  8. 1. Scare them! "If we don't refactor, then . !"

    costs will be much higher your maintenance Hard to quantify
  9. 3. Take hostages Feature #1 Feature #2 Blames business for

    rushing Technical Debt Technical.Debt
  10. 3. Take hostages Feature #1 Feature #2 Erodes trust in

    the team Technical Debt Technical.Debt
  11. 1. Refactoring Patterns • Extract method • Pull up /

    push down • Split loop Safer with good tools
  12. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true Rewriting in big steps is confusing & error-prone
  13. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true Heavy monitoring & analysis required
  14. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true Experimenting on humans is risky
  15. TDD

  16. 9

  17. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end We will create our "seam" here
  18. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  19. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  20. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  21. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  22. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  23. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  24. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  25. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end This seam is more complex
  26. Cut

  27. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ] :old must respond_to?(:call)
  28. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ] Initially a no-op; verify it still works
  29. calc = Calculator.new params[:nums].each {|n| calc.tally(n) :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  30. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  31. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  32. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total Wait, calc isn't an arg!
  33. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  34. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total Broaden the seam
  35. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  36. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  37. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  38. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total Return a value
  39. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  40. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ], record_calls: true
  41. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ], record_calls: true
  42. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ], record_calls: true Most options support ENV: SUTURE_RECORD_CALLS=true
  43. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  44. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  45. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  46. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  47. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  48. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  49. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  50. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  51. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n], record_calls: true } @result = calc.total
  52. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n], record_calls: true } @result = calc.total
  53. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  54. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  55. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  56. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  57. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  58. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  59. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  60. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  61. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  62. require "suture" class ItemsController < ApplicationController def update_all Item.all.each do

    |item| Suture.create :gilded_rose, :old => lambda { |item| item.update_quality! item }, :args => [item], :record_calls => true end redirect_to items_path end end
  63. class Calculator def add(left, right) right.times do left += 1

    end left end end Doesn't work for negative values!
  64. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end Retain current behavior exactly, bugs & all
  65. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end We don't know what else depends on bad behavior
  66. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  67. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end Skips odd values!
  68. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  69. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  70. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  71. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  72. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end Still returns nil
  73. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  74. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  75. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  76. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end "Make the change easy, then make the easy change" - Beck
  77. # Verification of your seam failed! Descriptions of each unsuccessful

    verification follows: ## Failures 1.) Recorded call for seam :tally (ID: 13) ran and failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ``` Ideas to fix this: * Focus on this test by setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)`
  78. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  79. Ideas to fix this: * Focus on this test by

    setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)`
  80. Ideas to fix this: * Focus on this test by

    setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)` Only run this failure
  81. Ideas to fix this: * Focus on this test by

    setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)` Delete bad recordings
  82. ### Fixing these failures #### Custom comparator If any comparison

    is failing and you believe the results are equivalent, we suggest you look into creating a custom comparator. See more details here: https://github.com/testdouble/suture#creating-a-custom-comparator #### Random seed Suture runs all verifications in random order by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  83. ### Fixing these failures #### Custom comparator If any comparison

    is failing and you believe the results are equivalent, we suggest you look into creating a custom comparator. See more details here: https://github.com/testdouble/ suture#creating-a-custom-comparator
  84. ==

  85. or

  86. !=

  87. !=

  88. !=

  89. class CalcPare < Suture::Comparator def call(left, right) if super then

    return true end left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  90. class CalcPare < Suture::Comparator def call(left, right) if super then

    return true end left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  91. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  92. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  93. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  94. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  95. #### Random seed Suture runs all verifications in random order

    by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  96. #### Random seed Suture runs all verifications in random order

    by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  97. #### Random seed Suture runs all verifications in random order

    by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  98. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  99. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  100. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  101. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  102. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  103. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  104. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  105. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  106. # Result Summary - Passed........12 - Failed........1 - with error..0

    - Skipped.......0 - Total calls...13 ## Progress Here's what your progress to initial completion looks like so far: [••••••••••••••••••••••••••••••••••••••◍◌◌◌◌] Of 13 recorded interactions, 12 are currently passing. That's 92%!
  107. # Result Summary - Passed........12 - Failed........1 - with error..0

    - Skipped.......0 - Total calls...13 ## Progress Here's what your progress to initial completion looks like so far: [••••••••••••••••••••••••••••••••••••••◍◌◌◌◌] Of 13 recorded interactions, 12 are currently passing. That's 92%!
  108. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  109. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  110. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  111. class Calculator attr_reader :total def new_tally(n) return if n.odd? #

    ^ FIXME later @total ||= 0 @total += n return end end
  112. class Calculator attr_reader :total def new_tally(n) return if n.odd? #

    ^ FIXME later @total ||= 0 @total += n return end end
  113. class Calculator attr_reader :total def new_tally(n) return if n.odd? #

    ^ FIXME later @total ||= 0 @total += n return end end
  114. class Calculator attr_reader :total def new_tally(n) @total ||= 0 return

    if n.odd? # ^ FIXME later @total ||= 0 @total += n return end end
  115. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  116. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  117. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  118. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end Calls :new & :old
  119. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end Calls :new & :old ✅
  120. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total
  121. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total
  122. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total
  123. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total ❌
  124. The results from the old & new code paths did

    not match for the seam (Suture::Error::ResultMismatch) :tally and Suture is raising this error because the `:call_both` option is enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=4>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ``` Here's what we recommend you do next: 1. Verify that this mismatch does not represent a missed requirement in the new code path. If it does, implement it! 2. If either (or both) code path has a side effect that impacts the return value of the other, consider passing an `:after_old` and/or `:after_new` hook to clean up your application's state well enough to run both paths one-after-the-other safely. 3. If the two return values above are sufficiently similar for the purpose of your application, consider writing your own custom comparator that relaxes the comparison (e.g. only checks equivalence of the attributes that matter). See the README for more info on custom comparators. 4. If the new code path is working as desired (i.e. the old code path had a bug for this argument and you don't want to reimplement it just to make them perfectly in sync with one another), consider writing a one-off comparator for this seam that will ignore the affected range of arguments. See the README for more info on custom comparators. By default, Suture's :call_both mode will log a warning and raise an error when the results of each code path don't match. It is intended for use in any pre-production environment to "try out" the new code path before pushing it to production. If, for whatever reason, this error is too disruptive and logging is sufficient for monitoring results, you may disable this error by setting `:raise_on_result_mismatch` to false.
  125. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  126. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  127. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  128. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  129. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  130. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  131. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  132. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total protect from arg mutation
  133. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total protect from arg mutation ☺
  134. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total protect from arg mutation ❌
  135. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  136. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total calc never changes now!
  137. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  138. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total total is always nil
  139. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  140. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  141. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  142. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  143. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total ✅
  144. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  145. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  146. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  147. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  148. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  149. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end Rescues :new with :old
  150. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end Rescues :new with :old ✅
  151. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  152. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  153. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  154. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  155. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total ✅
  156. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  157. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  158. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  159. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  160. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  161. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  162. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], ) fallback_on_error: true end end
  163. class Controller def show calc = Calculator.new @result = calc.new_add(

    old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], ) fallback_on_error: true end end
  164. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  165. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  166. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  167. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  168. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  169. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  170. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  171. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  172. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  173. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(n) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  174. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(n) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  175. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(n) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total