to_robust 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +39 -0
  3. data/lib/global/fix.rb +72 -0
  4. data/lib/global/fix/add_atom.rb +28 -0
  5. data/lib/global/fix/atom.rb +51 -0
  6. data/lib/global/fix/init.rb +4 -0
  7. data/lib/global/fix/remove_atom.rb +16 -0
  8. data/lib/global/fix/swap_atom.rb +26 -0
  9. data/lib/global/global.rb +155 -0
  10. data/lib/global/init.rb +10 -0
  11. data/lib/global/kernel/argument_error.rb +22 -0
  12. data/lib/global/kernel/exception.rb +9 -0
  13. data/lib/global/kernel/init.rb +4 -0
  14. data/lib/global/kernel/no_method_error.rb +15 -0
  15. data/lib/global/kernel/zero_division_error.rb +21 -0
  16. data/lib/global/report.rb +42 -0
  17. data/lib/global/robust_proc.rb +48 -0
  18. data/lib/global/strategies/divide_by_zero_error_strategy.rb +277 -0
  19. data/lib/global/strategies/init.rb +5 -0
  20. data/lib/global/strategies/no_method_error_strategy.rb +92 -0
  21. data/lib/global/strategies/wrong_arguments_error_strategy.rb +174 -0
  22. data/lib/global/strategy.rb +67 -0
  23. data/lib/local/init.rb +6 -0
  24. data/lib/local/kernel/init.rb +1 -0
  25. data/lib/local/kernel/module.rb +43 -0
  26. data/lib/local/local.rb +43 -0
  27. data/lib/local/strategies/bignum_division_strategy.rb +11 -0
  28. data/lib/local/strategies/bignum_modulus_strategy.rb +11 -0
  29. data/lib/local/strategies/fixnum_coerce_strategy.rb +11 -0
  30. data/lib/local/strategies/fixnum_division_strategy.rb +12 -0
  31. data/lib/local/strategies/fixnum_modulus_strategy.rb +12 -0
  32. data/lib/local/strategies/float_division_strategy.rb +11 -0
  33. data/lib/local/strategies/float_modulus_strategy.rb +12 -0
  34. data/lib/local/strategies/init.rb +37 -0
  35. data/lib/local/strategies/numeric_division_strategy.rb +11 -0
  36. data/lib/local/strategies/numeric_modulus_strategy.rb +12 -0
  37. data/lib/local/strategies/soft_binding_strategy.rb +108 -0
  38. data/lib/local/strategies/swap_method_strategy.rb +32 -0
  39. data/lib/local/strategy.rb +18 -0
  40. data/lib/to_robust.rb +4 -0
  41. data/test/local/float_division.rb +28 -0
  42. data/test/local/integer_division.rb +28 -0
  43. data/test/local/method_binding.rb +45 -0
  44. metadata +109 -0
@@ -0,0 +1,174 @@
1
+ # Robustness strategy for dealing with ArgumentError's thrown due to incorrect numbers of
2
+ # arguments being supplied to a method. Trims or pads the list of arguments so that the number
3
+ # of arguments is equal to the arity of the method (i.e. the number of provided arguments
4
+ # matches the expected number of arguments).
5
+ class ToRobust::Global::Strategies::WrongArgumentsErrorStrategy < ToRobust::Global::Strategy
6
+
7
+ # Fixes all calls to a given method on a provided line such that they all use
8
+ # an expected number of arguments. Method calls with too few parameters are padded with zeroes,
9
+ # whilst method calls with too many parameters are trimmed to the correct size.
10
+ #
11
+ # *Parameters:*
12
+ # * method, the method to fix calls to.
13
+ # * arity, the arity (expected number of parameters) of the method.
14
+ # * line, the line to fix method calls on.
15
+ #
16
+ # *Returns:*
17
+ # The fixed form of the line, with all calls to the given method meeting the argument length requirements.
18
+ def self.fix_calls(method, arity, line)
19
+
20
+ # Extract all calls to the affected method on the given line.
21
+ calls = extract_calls(line)
22
+ calls.reject! do |c|
23
+ method_full_name = line[c[0]...line.index('(', c[0])].split(/\.|::/)
24
+ method_full_name.last != method
25
+ end
26
+
27
+ # Re-order calls so that nested calls are processed first.
28
+ # First, order the calls by their starting position.
29
+ # Secondly, create a new list and insert each call at an smaller
30
+ # index to any calls that enclose it.
31
+ calls.sort!{|a,b| a[0] <=> b[0]}
32
+ temp = []
33
+ calls.each do |x|
34
+ index = insert_at = temp.length
35
+ if insert_at > 0
36
+ temp.reverse_each do |y|
37
+ index -= 1
38
+ insert_at = index if y[0] < x[0] and y[1] > x[1]
39
+ end
40
+ end
41
+ temp.insert(insert_at, x)
42
+ end
43
+ calls = temp
44
+
45
+ # Process each of the calls (in the safe order that has
46
+ # been established). Begin by extracting the current arguments
47
+ # for the method call, then either padding or shrinking those
48
+ # arguments to meet the required length.
49
+ calls.each_index do |call_index|
50
+
51
+ meth_start, meth_end = calls[call_index]
52
+ params_start = line.index('(', meth_start)+1
53
+ arg_start = params_start
54
+
55
+ # Process each character in the body of parameters.
56
+ open_brackets = 0
57
+ arguments = []
58
+ for char_index in params_start...meth_end
59
+
60
+ # Extract the character at the given point in the call.
61
+ char = line[char_index]
62
+
63
+ # Listen for bracket closings.
64
+ if char == '('
65
+ open_brackets += 1
66
+
67
+ # Listen for any bracket openings.
68
+ elsif char == ')'
69
+ open_brackets -= 1
70
+
71
+ # Check if this character marks the end of the argument.
72
+ # Only interpret it as the end of the argument if there are
73
+ # no open brackets.
74
+ elsif char == ',' and open_brackets == 0
75
+ arguments << [arg_start, char_index-1]
76
+ arg_start = char_index + 1
77
+ end
78
+
79
+ end
80
+
81
+ # Add the last argument.
82
+ arguments << [arg_start, meth_end-1]
83
+
84
+ # Shrink the arguments to the limit (better to do this before
85
+ # extracting their text contents, small optimisation), then
86
+ # retrieve their context contents (stripping any leading and
87
+ # trailing whitespace) and pad the arguments with zeroes
88
+ # where necessary.
89
+ arguments = arguments.first(arity)
90
+ arguments.map!{|a_start, a_end| line[a_start..a_end].strip}
91
+ arguments.fill('0', arguments.length...arity)
92
+
93
+ # Apply the transformation, calculate the length difference (delta)
94
+ # and re-adjust the boundaries of the remaining method calls.
95
+ transformed = "#{line[meth_start...line.index('(', meth_start)]}(#{arguments.join(',')})"
96
+ delta = transformed.length - (meth_end - meth_start + 1)
97
+ line[meth_start..meth_end] = transformed
98
+ (call_index+1...calls.length).each do |succ_call_index|
99
+
100
+ # Find the start and end points of the call.
101
+ succ_call_start, succ_call_end = calls[succ_call_index]
102
+
103
+ # Move the end point of any call containing the call that
104
+ # has been transformed.
105
+ if succ_call_start < meth_start and succ_call_end > meth_end
106
+ calls[succ_call_index][1] += delta
107
+
108
+ # Move the start and end points of each call that
109
+ # starts after the end of this call. (You could embed this
110
+ # within the if statement above, but this is nicer to read).
111
+ elsif succ_call_start > meth_end
112
+ calls[succ_call_index][0] += delta
113
+ calls[succ_call_index][1] += delta
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ # Return the transformed line.
121
+ return line
122
+
123
+ end
124
+
125
+ # Used to fix ArgumentError exceptions by ensuring that all calls to a given method
126
+ # on the line that the error occurred on use the expected number of arguments.
127
+ #
128
+ # When too few arguments are used during a method call, those arguments are padded
129
+ # with zeroes to reach the correct number of parameters.
130
+ #
131
+ # When too many arguments are supplied to a method, those arguments are trimmed to
132
+ # the number of arguments expected by the method.
133
+ #
134
+ # *Parameters:*
135
+ # * method, the affected method.
136
+ # * error, the error whose root cause should be fixed.
137
+ #
138
+ # *Returns:*
139
+ # A (possibly empty) array of candidate fixes to the root cause of the error.
140
+ def generate_candidates(method, error)
141
+
142
+ # Ensure that the error is a ArgumentError caused by an incorrect number
143
+ # of arguments being supplied to a method.
144
+ return [] unless error.is_a? ArgumentError and error.message.include? 'wrong number of arguments'
145
+
146
+ # Extract details of the error.
147
+ affected_method = error.affected_method
148
+ args_expected = error.args_expected
149
+ line_no = error.line_no
150
+ line_contents = method.source[line_no]
151
+
152
+ # We validate the fix by ensuring that any further errors are not
153
+ # ArgumentErrors for the incorrect number of arguments on the same
154
+ # method and on the same line.
155
+ validator = lambda do |method, old_error, new_error|
156
+ return true unless new_error.is_a? ArgumentError
157
+ return true unless new_error.message.include? 'wrong number of arguments'
158
+ return true unless new_error.line_no == old_error.line_no
159
+ return true unless new_error.affected_method == old_error.affected_method
160
+ return false
161
+ end
162
+
163
+ # Compose the sole candidate fix for this error.
164
+ line_contents = self.class.fix_calls(affected_method, args_expected, line_contents)
165
+ return [
166
+ ToRobust::Global::Fix.new(
167
+ [ToRobust::Global::Fix::SwapAtom.new(line_no, line_contents)],
168
+ validator
169
+ )
170
+ ]
171
+
172
+ end
173
+
174
+ end
@@ -0,0 +1,67 @@
1
+ # Strategies are used by the global robustness layer to suggest candidate fixes to errors that occur
2
+ # within a given function.
3
+ class ToRobust::Global::Strategy
4
+
5
+ # Extracts the co-ordinates of all method calls on a given line.
6
+ #
7
+ # *Parameters:*
8
+ # * line, the line to extract method calls from.
9
+ #
10
+ # *Returns:*
11
+ # An array of co-ordinate pairs (each an array - could use a range?).
12
+ def self.extract_calls(line)
13
+
14
+ # Find the co-ordinates of every method call in the string.
15
+ stack = []
16
+ calls = []
17
+ (0..line.length).each do |index|
18
+
19
+ # Get the character at this index.
20
+ char = line[index]
21
+
22
+ # If this is the start of a method call,
23
+ # add the starting index to the stack.
24
+ if char == '('
25
+ stack << index
26
+
27
+ # If this is the end of a method call,
28
+ # pop the last index off the stack and combine
29
+ # it with the current index to give the co-ordinates
30
+ # of the call.
31
+ elsif char == ')'
32
+ calls << [stack.pop, index]
33
+ end
34
+
35
+ end
36
+
37
+ # Post-process the co-ordinates so that they start from the label
38
+ # of the method call rather than the bracket opening. Do this by
39
+ # moving towards the start of the original string until the method
40
+ # definition has finished (checked by looking at the character).
41
+ calls.each_index do |i|
42
+ start_at = calls[i][0]
43
+ until start_at == 0 do
44
+ char = line[start_at-1]
45
+ break if not (char.match(/^[[:alpha:]]$/) or ['_',':','.'].include? char)
46
+ start_at -= 1
47
+ end
48
+ calls[i][0] = start_at
49
+ end
50
+
51
+ return calls
52
+
53
+ end
54
+
55
+ # Generates a list of candidate fixes to a given problem.
56
+ #
57
+ # *Parameters:*
58
+ # * method, the affected method.
59
+ # * error, the error which occurred within the method.
60
+ #
61
+ # *Returns:*
62
+ # A (possibly empty) array of candidate solutions to fix the root of the error.
63
+ def generate_candidates(method, error)
64
+ raise NotImplementedError, 'No "generate_candidates" method was implemented by this Strategy.'
65
+ end
66
+
67
+ end
data/lib/local/init.rb ADDED
@@ -0,0 +1,6 @@
1
+ module ToRobust; end
2
+
3
+ require_relative 'kernel/init.rb'
4
+ require_relative 'local.rb'
5
+ require_relative 'strategy.rb'
6
+ require_relative 'strategies/init.rb'
@@ -0,0 +1 @@
1
+ require_relative 'module.rb'
@@ -0,0 +1,43 @@
1
+ class Module
2
+
3
+ # Hides all public module methods specific to this module by prepending their names
4
+ # with "__" and making them private.
5
+ def hide_methods!
6
+ singleton_class.public_instance_methods(false).each do |original|
7
+
8
+ # Ignore method missing.
9
+ next if original == :method_missing
10
+
11
+ # Prepend "__" to the method name and make it private.
12
+ hidden = ('__' + original.to_s).to_sym
13
+ singleton_class.send(:alias_method, hidden, original)
14
+ singleton_class.send(:private, hidden)
15
+
16
+ end
17
+ end
18
+
19
+ # Unhides all previously hidden methods, restoring the object/class/module to its
20
+ # original state.
21
+ def unhide_methods!
22
+ hidden_method_symbols.each do |sym|
23
+ original = sym.to_s[2..-1].to_sym
24
+ singleton_class.send(:alias_method, original, sym)
25
+ singleton_class.send(:remove_method, sym)
26
+ singleton_class.send(:public, original)
27
+ end
28
+ end
29
+
30
+ # Returns a hash of the hidden methods of this module (indexed by the original name
31
+ # of the methods, as strings).
32
+ def hidden_methods
33
+ Hash[hidden_method_symbols.map { |sym| [sym.to_s[2..-1], method(sym)] }]
34
+ end
35
+
36
+ private
37
+
38
+ # Returns an array of the symbols for each of the hidden methods (including their "__").
39
+ def hidden_method_symbols
40
+ singleton_class.private_instance_methods(false).select { |m| m.to_s.start_with? '__' }
41
+ end
42
+
43
+ end
@@ -0,0 +1,43 @@
1
+ # The Local robustness module is a slightly tailored version of the local robustness layer
2
+ # proposed in Chris Timperley's Master's Thesis.
3
+ #
4
+ # For now patches are enabled and disabled by checking the status of the "enabled" flag in
5
+ # the Local robustness module for each individual patch.
6
+ #
7
+ # A far nicer alternative would be to implement patches as instances of a Patch class.
8
+ # This class would then contain details of the target class and method as well as a
9
+ # lambda function implementing the patched form of the method.
10
+ #
11
+ # WARNING: Thread safety is a concern.
12
+ #
13
+ # Author: Chris Timperley
14
+ module ToRobust::Local
15
+
16
+ # List of strategies.
17
+ @strategies = []
18
+ class << self
19
+ attr_reader :strategies
20
+ end
21
+
22
+ # Executes a given block under local robustness protection.
23
+ #
24
+ # *Parameters:*
25
+ # * *contexts, depickled list of context objects to protect method calls for.
26
+ # * &block, the block to execute under local robustness protection.
27
+ #
28
+ # *Returns:*
29
+ # * The result of the block execution.
30
+ def self.protected(*contexts, &block)
31
+ @strategies.each { |s| s.prepare!(contexts) }
32
+ @strategies.each { |s| s.enable! }
33
+
34
+ begin
35
+ result = block.call
36
+ ensure
37
+ @strategies.each { |s| s.disable! }
38
+ end
39
+
40
+ return result
41
+ end
42
+
43
+ end
@@ -0,0 +1,11 @@
1
+ # Ensures that integer division returns 0 if the denominator is zero.
2
+ class ToRobust::Local::Strategies::BignumDivisionStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new BignumDivisionStrategy.
5
+ def initialize
6
+ super(Bignum, :div, :__div) do |other|
7
+ other.zero? ? 0 : __div(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ # Ensures that integer division returns 0 if the denominator is zero.
2
+ class ToRobust::Local::Strategies::BignumModulusStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new BignumModulusStrategy.
5
+ def initialize
6
+ super(Bignum, :%, :__mod) do |other|
7
+ other.zero? ? 0 : __mod(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ # Coerce nil to 0.
2
+ class ToRobust::Local::Strategies::FixnumCoerceStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new FixnumCoerceStrategy.
5
+ def initialize
6
+ super(Fixnum, :coerce, :__coerce) do |other|
7
+ other.nil? ? 0 : __coerce(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ # Ensures that Fixnum division never encounters zero division errors
2
+ # by returning zero when the denominator is zero.
3
+ class ToRobust::Local::Strategies::FixnumDivisionStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
4
+
5
+ # Constructs a new FixnumDivisionStrategy.
6
+ def initialize
7
+ super(Fixnum, :/, :__div) do |other|
8
+ other.zero? ? 0 : __div(other)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ # Ensures that the modulus operator returns zero if a divide
2
+ # by zero error would occur.
3
+ class ToRobust::Local::Strategies::FixnumModulusStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
4
+
5
+ # Constructs a new FixnumModulusStrategy.
6
+ def initialize
7
+ super(Fixnum, :%, :__mod) do |other|
8
+ other.zero? ? 0 : __mod(other)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,11 @@
1
+ # Ensures that any float divided by zero gives 0.0
2
+ class ToRobust::Local::Strategies::FloatDivisionStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new FloatDivisionStrategy.
5
+ def initialize
6
+ super(Float, :/, :__fdiv) do |other|
7
+ other.zero? ? 0.0 : __fdiv(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ # Ensures that the modulus operator returns zero if a divide
2
+ # by zero error would occur.
3
+ class ToRobust::Local::Strategies::FloatModulusStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
4
+
5
+ # Constructs a new FloatModulusStrategy.
6
+ def initialize
7
+ super(Float, :%, :__mod) do |other|
8
+ other.zero? ? 0.0 : __mod(other)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,37 @@
1
+ module ToRobust::Local::Strategies; end
2
+
3
+ # Load all the strategy definitions.
4
+ require_relative 'swap_method_strategy.rb'
5
+
6
+ require_relative 'numeric_division_strategy.rb'
7
+ require_relative 'numeric_modulus_strategy.rb'
8
+
9
+ require_relative 'bignum_division_strategy.rb'
10
+ require_relative 'bignum_modulus_strategy.rb'
11
+
12
+ require_relative 'fixnum_division_strategy.rb'
13
+ require_relative 'fixnum_modulus_strategy.rb'
14
+ require_relative 'fixnum_coerce_strategy.rb'
15
+
16
+ require_relative 'float_division_strategy.rb'
17
+ require_relative 'float_modulus_strategy.rb'
18
+
19
+ require_relative 'soft_binding_strategy.rb'
20
+
21
+ # Instantiate and attach each of them.
22
+ # This could be done automatically (following class definition) using
23
+ # the "defined" gem, but this may cause some compatibility issues.
24
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::NumericDivisionStrategy.new
25
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::NumericModulusStrategy.new
26
+
27
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::BignumDivisionStrategy.new
28
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::BignumModulusStrategy.new
29
+
30
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FixnumDivisionStrategy.new
31
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FixnumModulusStrategy.new
32
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FixnumCoerceStrategy.new
33
+
34
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FloatModulusStrategy.new
35
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FloatDivisionStrategy.new
36
+
37
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::SoftBindingStrategy.new