Yesterday, I posted an article entitled Break my code, please, wherein I posted a very fragile piece of code, with the challenge to find ways in which to break it.
What follows is a discussion of the code and why it is bad/fragile/easily broken…..
In the first place, methods like tr! and slice! behave differently than their non-bang’d relatives, tr and slice. In general, methods which end in ‘!’ act upon the object, potentially changing it. Also, by convention, if the method does not change the object, then nil is returned. In the code there are many cases where it is possible for values to be passed in which do not affect the object, thus returning nil. Because the methods are executed in order, with the result of the first being used as the object on which the next is performed, often methods can be invoked on nil (which doesn’t work very well). Also, beyond code which is brittle and prone to failure, there is also code which can potentially cause confusion.
I’m attaching a commented version, which contains rspec tests; I’ll also include it here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | class Fragile # not an error, but is this what we want? attr_reader :name # attr_reader is readonly; this might be ok, it # might not, it depends on what we want. # Initially, we all start in the same place.... # But we can get off to a bad start. def initialize(name) # obviously, if we don't pass in a # value this breaks @name = name end # Slice it; dice it; # what causes the salad to shoot the wrong way? def caesar_salad() # this next line can break for a single reason -- # if the name is already lowercase @name.downcase!.tr("abcdefghijklmnopqrstuvwxyz", "zyxwvutsrqponmlkjihgfedbca") end # Change is hard. # original can be set with different values # such that errors can be caused by every # line def morph(original) # obviously this breaks if no value is # passed to it # this can break if original is not a string s=original.upcase.gsub(/[^A-Z]/,"") # This will break if s does not contain A, B, or C s.tr!("ABC","123").squeeze! # If the string does not begin with a letter, this breaks # Also, if the string is less than 5 letters, slice! will # remove all the characters, so the next line fails s.capitalize!.slice!(0,5) s=s[0].succ end # Everyone has their limit # what will cause me to break? def counter(limit) # obviously not passing in a value # breaks it # This one is a little odd; we can break it by passing in a # string. Also, if you pass in a number less than one, it will # still evaluate the block once, which can be misleading. 1.upto(limit){ |n| puts n} end # Shifting into gear; if you can't find 'em, grind 'em def red_shift(num,shift) # obviously missing arguments will # break it # This will fail if both arguments are not numeric. (0 + num ) << shift # Additionally, it behaves oddly if a negative number is shifted # by a negative number of bits. It will never go to 0, but rather # to -1 end # If I'm not careful, I'll lose a sandal.... # what will break me? def hermes(message) # will break if we don't pass in an arg # all classes have the inspect method (including nil) so we can't # make it fail that way. However, if a class is passed in (or # nil) it will use the name of the class. Arrays and hashes work, # but objects may look weird -- inspect invokes the to_s method # which, unless overridden contains an object's object_id self.send(:sos,message.inspect) end # I may not break, but I have issues... def method_missing(symbol,*args) # again, arguments case symbol when :sos # I've changed this from the original; to better illustrate -- # originally it had a puts; it now returns the string. # This can behave oddly if nothing is passed; it won't break, # but it's just as though a message was not put in the bottle. "I'll send an SOS to the world;\nMy message in a bottle is:\n#{args[0]}" end # One major issue is that we are not invoking the super class' # method_missing. As such, if we attempt to invoke any method not # defined, other than :sos, then nil will be returned. end end describe "Fragile code breaks easily" do before(:each) do @fragile = Fragile.new("Matt") end describe "the constructor" do it "should fail when no name is passed in" do lambda{@fragile = Fragile.new}.should raise_error(ArgumentError) end end describe "our attribute" do it "should allow us to read the attribute" do (@fragile.name).should == ("Matt") end it "should not allow us to read the attribute" do (@fragile.name = "fred").should == ("fred") end end describe "caesar_salad" do it "will fail when a lowercase name is passed in because of the downcase!" do fragile = Fragile.new("matt") lambda{fragile.caesar_salad()}.should raise_error(NoMethodError) end it "should not fail when a mixed case name is passed in" do (@fragile.caesar_salad).should == ("nzgg") end end describe "morph" do it "will fail when no arguments are passed in" do lambda{@fragile.morph()}.should raise_error(ArgumentError) end it "will fail when a non-string is passed in" do lambda{@fragile.morph(1234)}.should raise_error(NoMethodError) end it "will fail when nil is passed in (can't call upcase on nil)" do lambda{@fragile.morph(nil)}.should raise_error(NoMethodError) end it "will fail when a string not containing alpha characters is passed in (the gsub strips any non alpha)" do lambda{@fragile.morph("12345")}.should raise_error(NoMethodError) end it "will fail when a string not containing a, b, or c is not passed in (tr! returns nil if there are no matches)" do lambda{@fragile.morph("dfegr")}.should raise_error(NoMethodError) end it "will fail when a string which cannot be capitalized is passed in (capitalize!)" do lambda{@fragile.morph("a1234")}.should raise_error(NoMethodError) end it "will fail when a string <= 5 characters is passed in (slice! and s[0])" do lambda{@fragile.morph("scar")}.should raise_error(NoMethodError) end it "should succeed when 'This is a test' is passed into it." do (@fragile.morph("This is a test")).should == (116) end end # this is a really poor method, come to think about it..... I # should have had it return something for real, but since the # code was posted, I'll live with it... describe "counter" do it "will fail when no variable is provided" do lambda{@fragile.counter()}.should raise_error(ArgumentError) end it "will fail when a string is provided" do lambda{@fragile.counter("a")}.should raise_error(ArgumentError) end it "will not provide any output, but returns 1 when limit < 1" do (@fragile.counter(0)).should == (1) end it "will count once when limit = 1" do (@fragile.counter(0)).should == (1) end it "will count more than once when limit > 1" do (@fragile.counter(2)).should == (1) end end describe "red_shift" do it "will complain if there are not enough arguments" do lambda{@fragile.red_shift()}.should raise_error(ArgumentError) lambda{@fragile.red_shift(1)}.should raise_error(ArgumentError) end it "will fail if both arguments are not numbers" do lambda{@fragile.red_shift(1,"a")}.should raise_error(TypeError) lambda{@fragile.red_shift("a", 1)}.should raise_error(TypeError) end it "will return -1 if -2 is shifted a negative amount (while this doesn't break, it's not intuitive)" do (@fragile.red_shift(-2,-5)).should == (-1) end it "will return a number less than 0 if a negative number is shifted" do (@fragile.red_shift(-1,1)).should == (-2) end end describe "hermes" do it "will return a message if passed 'help'" do (@fragile.hermes("help")).should == ("I'll send an SOS to the world;\nMy message in a bottle is:\n\"help\"") end it "will return a message including the name of a class if passed in a class" do (@fragile.hermes(Class)).should == ("I'll send an SOS to the world;\nMy message in a bottle is:\nClass") end it "will complain if there are not enough arguments" do lambda{@fragile.hermes()}.should raise_error(ArgumentError) end end describe "method_missing" do it "will fail if we don't send a symbol" do lambda{@fragile.method_missing}.should raise_error(ArgumentError) end it "will work if we send sos with a message" do (@fragile.send(:sos, "help me!")).should == ("I'll send an SOS to the world;\nMy message in a bottle is:\nhelp me!") end it "will work, even if we don't want it to, if we don't put a message" do (@fragile.send(:sos)).should == ("I'll send an SOS to the world;\nMy message in a bottle is:\n") end it "will not do anything if we pass anything other than :sos (we're not calling super.method_missing)" do (@fragile.send(:fred)).should be_nil end end end |
In conclusion, I’d like to highlight the following:
- Differences between methods ending in ‘!’ and their non-adorned cousins
method_missingand it’ssuper- Not all types/objects respond to the same message — it’s a good idea to innoculate yourself.
- Sometimes, behaviour is non-intuitive, so testing for boundary cases is a good idea.
I’d love to see other examples of “fragile” code, if anyone cares to share.



