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
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Chris Timperley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ ruby_to_robust
2
+ ==============
3
+
4
+ RubyToRobust allows you to implement soft semantics in your programs so that they
5
+ may automatically recover from a range of supported exceptions in some sensible manner,
6
+ allowing the program to continue running.
7
+
8
+ The soft semantics provided by this project were originally designed to be exploited by
9
+ Wallace.rb to provide robustness and support for soft grammars when using Grammatical Evolution.
10
+
11
+ Two different soft semantics solutions are provided by RubyToRobust, both based on the
12
+ Local and Global robustness schemes outlined in Chris Timperley's Masters Thesis.
13
+
14
+ * **Global:** The Global robustness module implements soft semantics by intercepting
15
+ all exceptions thrown by a monitored method and using line information and other information
16
+ provided by the exception to determine the root of the problem in the method source code
17
+ which it then proceeds to "repair".
18
+ * **Local:** Rather than manipulating the source code of the monitored function, the Local
19
+ robustness measure operates by implementing a series of monkey patches which ensure that
20
+ a set of supported exceptions are never (or rarely) thrown within the context of the monitored
21
+ function.
22
+
23
+ Please note that the definition of a "repaired method" used by this project is a method which
24
+ no longer produces errors (which would otherwise crash the program). This does not mean that the
25
+ program does what its programmer wanted (although sometimes it might).
26
+
27
+ The implementation and behaviour of this robustness scheme is very different to the implementation
28
+ used in Chris Timperley's Master Thesis.
29
+ * In the original version Global robustness was either enabled or disabled, and was constantly active
30
+ except in certain classes and modules. The new implementation allows Global robustness to be constrained
31
+ to a given block or method, so that the program may operate as it otherwise would outside the "protected"
32
+ block.
33
+ * Exceptions are treated as soon as they are thrown outside the context of the monitored method.
34
+ The original Global robustness would allow exceptions to propagate through the program (allowing
35
+ them to be caught by a parent context) up till the point they would crash Ruby.
36
+ * Performance is hugely improved! By providing the optional file name and line number parameters to
37
+ dynamically evaluated procedures there is no need to use external files to extract detailed error information.
38
+ Additionally, since the soft semantics are constrained to a given block or method, very few method calls
39
+ have to be wrapped (and therefore slowed down).
data/lib/global/fix.rb ADDED
@@ -0,0 +1,72 @@
1
+ # Instances of this class are used to represent candidate fixes / solutions to given errors that
2
+ # have occurred during the execution of a method.
3
+ #
4
+ # A fix operates on the source code of a method, adding, removing and replacing specific lines of
5
+ # code (like a Git patch/change) in an attempt to remove the source of the error.
6
+ #
7
+ # As mentioned elsewhere in the Global module, strategies may return an ordered sequence of fixes
8
+ # according to the likelihood that they will solve the problem (or by their performance penalty to
9
+ # the method), but as this ordering is carried out by the strategies and all ordering information is
10
+ # implict, candidate fixes proposed by different solutions cannot be compared and ordered.
11
+ #
12
+ # The ability to compare arbitrary fixes could be introduced by adding a notion of cost to each fix;
13
+ # a net quantification of both the performance penalty incurred to the method *after* applying the fix
14
+ # and the probability that the fix will be a good one (a good fix would have a lower cost than a worse
15
+ # fix). Candidate fixes could then be processed in ascending order of cost.
16
+ class ToRobust::Global::Fix
17
+
18
+ # Creates a new candidate fix.
19
+ #
20
+ # *Parameters:*
21
+ # * changes, an array of changes that the fix should make to the source code.
22
+ # * validator, a lambda function that verifies a given error is not the same as the original error.
23
+ def initialize(changes,validator)
24
+ @changes = changes
25
+ @validator = validator
26
+ end
27
+
28
+ # Calculates the source code of the fixed method using the atomic fixes described by this object.
29
+ #
30
+ # *Parameters:*
31
+ # * src, the original source code (as lines) of the affected method.
32
+ #
33
+ # *Returns:*
34
+ # The fixed source code for the method.
35
+ def fixed_source(src)
36
+ history = []
37
+ @changes.each do |change|
38
+ change.apply!(src, history)
39
+ history << change
40
+ end
41
+ return src
42
+ end
43
+
44
+ # Applies the proposed changes to the source code of the affected method.
45
+ #
46
+ # *Parameters:*
47
+ # * method, the affected method.
48
+ #
49
+ # *Returns*
50
+ # A variant of the affected method modified according to the changes given by this fix.
51
+ def apply(method)
52
+ ToRobust::Global::RobustProc.new(method.headers, fixed_source(method.source.dup[1...-1]).join('/\n/)'))
53
+ end
54
+
55
+ # In the event that the method resulting from the fix encounters an error, this method *attempts*
56
+ # to determine whether the fix has been successful in removing the source of the original error.
57
+ # A "fixed" method in this sense is not necessarily one which removes all errors from a given method,
58
+ # rather it removes a particular identifiable error.
59
+ #
60
+ # *Parameters:*
61
+ # * method, the affected method.
62
+ # * original_error, the original error that was encountered by the method.
63
+ # * new_error, the error encountered after applying this fix and executing the method again.
64
+ #
65
+ # *Returns:*
66
+ # * True if the fix was successful in removing the cause of the original error, false if not.
67
+ def successful?(method, original_error, new_error)
68
+ @validator[method, original_error, new_error]
69
+ end
70
+ alias_method :validate, :successful?
71
+
72
+ end
@@ -0,0 +1,28 @@
1
+ # A add atom is used to insert a sequence of lines at a given position in the affected source code.
2
+ class ToRobust::Global::Fix::AddAtom < ToRobust::Global::Fix::Atom
3
+
4
+ # Constructs a new add atom.
5
+ #
6
+ # *Parameters:*
7
+ # * line_no, the line in the source code which the atom should operate on.
8
+ # * additions, the sequence of lines to insert at the given line number (as an ordered array).
9
+ def initialize(line_no, additions)
10
+ super(line_no)
11
+ @additions = additions
12
+ end
13
+
14
+ # Calculates and returns the number of lines added by the atom.
15
+ def lines_added
16
+ @additions.length
17
+ end
18
+
19
+ # Inserts a sequence of lines at specific line in the affected source code.
20
+ #
21
+ # *Parameters:*
22
+ # * source, the (partially fixed) affected source.
23
+ # * changes, an array of the atomic changes already applied to this source (in order of application).
24
+ def apply!(source, changes)
25
+ source.insert(adjusted_line_no(changes), *@additions)
26
+ end
27
+
28
+ end
@@ -0,0 +1,51 @@
1
+ # A fix atom is used to store and represent a single "atomic" change made by a candidate fix which
2
+ # may be represented as a collection of such atomic fixes.
3
+ class ToRobust::Global::Fix::Atom
4
+
5
+ # The line which this atomic fix changes.
6
+ attr_reader :line_no
7
+
8
+ # Constructs a new fix atom.
9
+ #
10
+ # *Parameters:*
11
+ # * line_no, the line in the source code which the atom should operate on.
12
+ def initialize(line_no)
13
+ @line_no = line_no
14
+ end
15
+
16
+ # Calculates the number of lines that will be added by this fix.
17
+ def lines_added
18
+ raise NotImplementedError, 'No "lines_added" method was implemented by this Atom.'
19
+ end
20
+
21
+ # Calculate the line number in the adjusted source code that corresponds to the specified
22
+ # line in the body of the original source code.
23
+ #
24
+ # *Parameters:*
25
+ # * changes, an array of the atomic changes already applied to this source.
26
+ def adjusted_line_no(changes)
27
+
28
+ adj_n = @line_no
29
+ changes.each do |c|
30
+ if c.line_no < @line_no and lines_added < 0
31
+ adj_n += c.lines_added
32
+ elsif c.line_no >= line_no and lines_added > 0
33
+ adj_n += c.lines_added
34
+ end
35
+ end
36
+
37
+ # Subtract one to return the line number in the body (header takes up the first line).
38
+ return adj_n - 1
39
+
40
+ end
41
+
42
+ # Destructively applies the change described by this atomic fix to the given source code.
43
+ #
44
+ # *Parameters:*
45
+ # * source, the source code of the affected method (represented as a sequence of lines).
46
+ # * changes, an array of the atomic changes already applied to this source (in order of application).
47
+ def apply!(source, changes)
48
+ raise NotImplementedError, 'No "apply!" method was implemented by this Atom.'
49
+ end
50
+
51
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'atom.rb'
2
+ require_relative 'remove_atom.rb'
3
+ require_relative 'swap_atom.rb'
4
+ require_relative 'add_atom.rb'
@@ -0,0 +1,16 @@
1
+ # A remove atom is used to remove a line from the affected source code.
2
+ class ToRobust::Global::Fix::RemoveAtom < ToRobust::Global::Fix::Atom
3
+
4
+ # Removes a single line from the source code.
5
+ def lines_added; -1; end
6
+
7
+ # Removes a specific line from the affected source code.
8
+ #
9
+ # *Parameters:*
10
+ # * source, the (partially fixed) affected source.
11
+ # * changes, an array of the atomic changes already applied to this source (in order of application).
12
+ def apply!(source, changes)
13
+ source.delete_at(adjusted_line_no(changes))
14
+ end
15
+
16
+ end
@@ -0,0 +1,26 @@
1
+ # A remove atom is used to swap a line from the affected source code with another.
2
+ class ToRobust::Global::Fix::SwapAtom < ToRobust::Global::Fix::Atom
3
+
4
+ # Constructs a new swap atom.
5
+ #
6
+ # *Parameters:*
7
+ # * line_no, the line in the source code which the atom should operate on.
8
+ # * replacement, the contents of the replacement line.
9
+ def initialize(line_no, replacement)
10
+ super(line_no)
11
+ @replacement = replacement
12
+ end
13
+
14
+ # No lines are added.
15
+ def lines_added; 0; end
16
+
17
+ # Swaps the contents at specific line with an altered form stored by this object.
18
+ #
19
+ # *Parameters:*
20
+ # * source, the (partially fixed) affected source.
21
+ # * changes, an array of the atomic changes already applied to this source (in order of application).
22
+ def apply!(source, changes)
23
+ source[adjusted_line_no(changes)] = @replacement
24
+ end
25
+
26
+ end
@@ -0,0 +1,155 @@
1
+ # The Global robustness module implements a simplified (and faster) variant of the global
2
+ # robustness measure outlined in Chris Timperley's Master's Thesis.
3
+ #
4
+ # Methods executed under global robustness protection are executed within an exception
5
+ # catching block which intercept any errors which cause the method to fail
6
+ #
7
+ # For now this measure only works with simple methods that do not change the state of
8
+ # objects or interfere with the global state.
9
+ #
10
+ # Author: Chris Timperley
11
+ module ToRobust::Global
12
+
13
+ @strategies = []
14
+ @max_attempts = 10
15
+ @save_repairs = true
16
+
17
+ class << self
18
+
19
+ # A flag to control whether repairs to a method should be permanent (true) or if they
20
+ # should last only for a given call (false).
21
+ # True by default.
22
+ attr_accessor :save_repairs
23
+
24
+ # The maximum number of (different) errors that will be tolerated before
25
+ # all attempts to repair the method are ceased.
26
+ # 10 by default.
27
+ attr_accessor :max_attempts
28
+
29
+ # Allow strategies to be added and removed via:
30
+ # * Global.strategies << Strategy.new
31
+ # * Global.strategies.remove(strategy)
32
+ attr_reader :strategies
33
+
34
+ end
35
+
36
+ # Executes a given method (or proc) under global robustness protection.
37
+ #
38
+ # *Parameters:*
39
+ # * method, the (robust proc) method to execute using global robustness protection.
40
+ # * params, the parameters to the method call.
41
+ #
42
+ # *Returns:*
43
+ # The result of the method call.
44
+ def self.execute(method, params)
45
+
46
+ # Attempt to execute the method. If an error occurs then attempt to repair the method
47
+ # using the error information.
48
+ attempts = 0
49
+ begin
50
+ return method.call(*params.clone)
51
+ rescue => original_error
52
+ report = repair(method, params, original_error)
53
+ attempts += 1
54
+ end
55
+
56
+ # Once repaired attempt to call the method again, repeating the cycle,
57
+ # until either the method successfully returns a result or a maximum number of errors occur.
58
+ until attempts >= @max_attempts or report.complete?
59
+ attempts += 1
60
+ report = repair(report.fixed_method, params, report.error)
61
+ end
62
+
63
+ # If the repair limit has been hit then return the original error and make no permanent
64
+ # changes to the supplied method (or should we keep the changes?).
65
+ raise original_error if report.partial?
66
+
67
+ # If the method was completely repaired then swap the original method with the fixed form
68
+ # (so long as that functionality is enabled) and return the result of the method call.
69
+ method.become(report.fixed_method) if @save_repairs
70
+ return report.result
71
+
72
+ end
73
+
74
+ # Calculates all the possible candidate fixes to a given error within a provided method.
75
+ # Passes the details of the error along with the method to each associated error handling
76
+ # strategy and queries all possible candidate solutions.
77
+ #
78
+ # *TODO:*
79
+ # At the moment each strategy may return an ordered set of candidate fixes to the problem,
80
+ # but this ordering information is implicit (and usually not universally comparable) so there
81
+ # can be no ordering of candidate fixes from ALL given strategies (i.e. strategy B may produce
82
+ # a strategy that is more likely to solve the error than the best candidate given by strategy A).
83
+ #
84
+ # *Parameters:*
85
+ # * method, the method to be fixed.
86
+ # * error, the error to be addressed.
87
+ #
88
+ # *Returns:*
89
+ # An array of candidate fixes to the error.
90
+ def self.generate_candidates(method, error)
91
+ @strategies.map { |s| s.generate_candidates(method, error) }.flatten
92
+ end
93
+
94
+ # Repairs a given (statically defined) method using the details of an encountered error.
95
+ #
96
+ # Special care must be taken to define repair. In this context, a repaired method
97
+ # is one which does not suffer from the same error that it did originally. This means
98
+ # that the repaired form of the provided method may still contain errors.
99
+ #
100
+ # This definition of repair is necessary to fix methods which contain more than a single
101
+ # error.
102
+ #
103
+ # *Parameters:*
104
+ # * method, the broken method to be repaired.
105
+ # * params, the parameters to the original method call.
106
+ # * error, the error that occurred when the method was called.
107
+ #
108
+ # *Returns:*
109
+ # A repaired form of the method.
110
+ def self.repair(method, params, error)
111
+
112
+ # Produce a series of candidate fixes to the method.
113
+ candidates = generate_candidates(method, error)
114
+
115
+ # Attempt each of the candidate fixes until the error is prevented or there are no
116
+ # further candidates to try.
117
+ until candidates.empty?
118
+
119
+ fix = candidates.shift
120
+ fixed_method = fix.apply(method)
121
+
122
+ begin
123
+ result = fixed_method.call(*params.clone)
124
+ return Report.new(fixed_method, result: result)
125
+ rescue => fix_error
126
+ return Report.new(fixed_method, error: fix_error) if fix.successful?(fixed_method, error, fix_error)
127
+ end
128
+
129
+ end
130
+
131
+ # If all attempts to repair the method against the given error fail then re-throw
132
+ # the exception.
133
+ raise error
134
+
135
+ end
136
+
137
+ # Executes a given block whilst preventing a ZeroDivisionError occurring.
138
+ # If a zero division error does occur during the execution of the block, this safety
139
+ # wrapper will catch the exception and will return a zero instead, allowing the program
140
+ # to continue.
141
+ #
142
+ # *Parameters:*
143
+ # * &block, the block to execute under ZeroDivisionError protection.
144
+ #
145
+ # *Returns:*
146
+ # The result of the block execution, or zero if the block execution yields a ZeroDivisionError.
147
+ def self.prevent_dbz(&block)
148
+ begin
149
+ return block.call
150
+ rescue ZeroDivisionError
151
+ return 0
152
+ end
153
+ end
154
+
155
+ end
@@ -0,0 +1,10 @@
1
+ module ToRobust; end
2
+
3
+ require_relative 'kernel/init.rb'
4
+ require_relative 'global.rb'
5
+ require_relative 'robust_proc.rb'
6
+ require_relative 'strategy.rb'
7
+ require_relative 'strategies/init.rb'
8
+ require_relative 'fix.rb'
9
+ require_relative 'fix/init.rb'
10
+ require_relative 'report.rb'
@@ -0,0 +1,22 @@
1
+ # Augments the functionality of ArgumentError instances so that the affected method and expected number
2
+ # of arguments can be quickly inspected. Also adjusts the line number calculation so that the correct
3
+ # line number is returned for the error.
4
+ class ArgumentError
5
+
6
+ # Returns the name of the method which was provided with poorly formed arguments.
7
+ def affected_method
8
+ backtrace[0].match(/`(([a-zA-Z]|_)+)'/)[0][1...-1]
9
+ end
10
+
11
+ # Returns the number of arguments that were expected by this method.
12
+ def args_expected
13
+ message.match(/\s(\d+)\)/)[0][1...-1].to_i
14
+ end
15
+
16
+ # Returns the line number that the method call was made from.
17
+ # The line that the method call was made is given by the second line of the backtrace.
18
+ def line_no
19
+ backtrace[1][/:(\d+):/][1...-1].to_i
20
+ end
21
+
22
+ end