to_robust 1.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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