Class: Chaser

Inherits:
Object
  • Object
show all
Defined in:
lib/zombie-chaser/chaser.rb

Overview

Test Unit Sadism

Direct Known Subclasses

ZombieTestChaser

Defined Under Namespace

Classes: Reporter, Timeout

Constant Summary collapse

VERSION =

The version of Chaser you are using.

'0.0.4'
WINDOZE =

Is this platform MS Windows-like?

RUBY_PLATFORM =~ /mswin/
NULL_PATH =

Path to the bit bucket.

WINDOZE ? 'NUL:' : '/dev/null'
@@debug =
false
@@guess_timeout =
true
@@timeout =

default to something longer (can be overridden by runners)

60

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass_name = nil, method_name = nil, reporter = Reporter.new) ⇒ Chaser

Creates a new Chaser that will chase klass_name and method_name, sending results to reporter.



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/zombie-chaser/chaser.rb', line 88

def initialize(klass_name = nil, method_name = nil, reporter = Reporter.new)
  @klass_name = klass_name
  @method_name = method_name.intern if method_name

  @klass = klass_name.to_class if klass_name

  @method = nil
  @reporter = reporter

  @mutated = false

  @failure = false
end

Instance Attribute Details

#klassObject

Class being chased



36
37
38
# File 'lib/zombie-chaser/chaser.rb', line 36

def klass
  @klass
end

#klass_nameObject

Name of class being chased



41
42
43
# File 'lib/zombie-chaser/chaser.rb', line 41

def klass_name
  @klass_name
end

#methodObject

Method being chased



46
47
48
# File 'lib/zombie-chaser/chaser.rb', line 46

def method
  @method
end

#method_nameObject

Name of method being chased



51
52
53
# File 'lib/zombie-chaser/chaser.rb', line 51

def method_name
  @method_name
end

#old_methodObject (readonly)

The original version of the method being chased



56
57
58
# File 'lib/zombie-chaser/chaser.rb', line 56

def old_method
  @old_method
end

Class Method Details

.debugObject



62
63
64
# File 'lib/zombie-chaser/chaser.rb', line 62

def self.debug
  @@debug
end

.debug=(value) ⇒ Object



66
67
68
# File 'lib/zombie-chaser/chaser.rb', line 66

def self.debug=(value)
  @@debug = value
end

.guess_timeout?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'lib/zombie-chaser/chaser.rb', line 75

def self.guess_timeout?
  @@guess_timeout
end

.timeout=(value) ⇒ Object



70
71
72
73
# File 'lib/zombie-chaser/chaser.rb', line 70

def self.timeout=(value)
  @@timeout = value
  @@guess_timeout = false # We've set the timeout, don't guess
end

Instance Method Details

#aliasing_class(method_name) ⇒ Object

Convenience methods



275
276
277
# File 'lib/zombie-chaser/chaser.rb', line 275

def aliasing_class(method_name)
  method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
end

#calculate_proxy_method_name(original_name) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/zombie-chaser/chaser.rb', line 148

def calculate_proxy_method_name(original_name)
  result = "__chaser_proxy__#{original_name}"
  character_renaming = {"[]" => "square_brackets", "^" => "exclusive_or",
  "=" => "equals", "&" => "ampersand", "*" => "splat", "+" => "plus",
  "-" => "minus", "%" => "percent", "~" => "tilde", "@" => "at",
  "/" => "forward_slash", "<" => "less_than", ">" => "greater_than"}
  character_renaming.each do |characters, renamed_string_portion|
    result.gsub!(characters, renamed_string_portion)
  end
  result
end

#clean_method_nameObject



279
280
281
# File 'lib/zombie-chaser/chaser.rb', line 279

def clean_method_name
  method_name.to_s.gsub(/self\./, '')
end

#interface_puts(*args) ⇒ Object

For the benefit of puts calls in ZombieTestChaser.validate



80
81
82
# File 'lib/zombie-chaser/chaser.rb', line 80

def interface_puts(*args)
  @reporter.interface_puts(*args)
end

#modify_class_methodObject



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
# File 'lib/zombie-chaser/chaser.rb', line 204

def modify_class_method
  chaser = self
  @mutated = true
  @old_method = aliasing_class(@method_name).instance_method(clean_method_name)

  aliasing_class(@method_name).class_eval do
    remove_method(chaser.clean_method_name.to_sym)
  end

  chaser_proxy_method_name = calculate_proxy_method_name(clean_method_name)
  workaround_method_code_string = <<-EOM
    def #{@method_name}(*args, &block)
      #{chaser_proxy_method_name}(block, *args)
    end
  EOM
  @klass.class_eval do
    eval(workaround_method_code_string)
  end
  aliasing_class(@method_name).send(:define_method, chaser_proxy_method_name) do |block, *args|
    original_value = chaser.old_method.bind(self).call(*args) do |*yielded_values|
      mutated_yielded_values = yielded_values.map{|value| chaser.mutate_value(value)}
      block.call(*mutated_yielded_values)
    end
    chaser.mutate_value(original_value)
  end
end

#modify_instance_methodObject

Ruby 1.8 doesn’t allow define_method to handle blocks. The blog post coderrr.wordpress.com/2008/10/29/using-define_method-with-blocks-in-ruby-18/ show that define_method has problems, and showed how to do workaround_method_code_string



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/zombie-chaser/chaser.rb', line 181

def modify_instance_method
  chaser = self
  @mutated = true
  @old_method = @klass.instance_method(@method_name)
  chaser_proxy_method_name = calculate_proxy_method_name(@method_name)
  workaround_method_code_string = <<-EOM
    def #{@method_name}(*args, &block)
      #{chaser_proxy_method_name}(block, *args)
    end
  EOM
  @klass.class_eval do
    remove_method(chaser.clean_method_name.to_sym)
    eval(workaround_method_code_string)
  end
  @klass.send(:define_method, chaser_proxy_method_name) do |block, *args|
    original_value = chaser.old_method.bind(self).call(*args) do |*yielded_values|
      mutated_yielded_values = yielded_values.map{|value| chaser.mutate_value(value)}
      block.call(*mutated_yielded_values)
    end
    chaser.mutate_value(original_value)
  end
end

#modify_methodObject



231
232
233
234
235
236
237
# File 'lib/zombie-chaser/chaser.rb', line 231

def modify_method
  if method_name.to_s =~ /self\./
    modify_class_method
  else
    modify_instance_method
  end
end

#mutate_value(value) ⇒ Object

Replaces the value with a random value.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/zombie-chaser/chaser.rb', line 251

def mutate_value(value)
  case value
  when Fixnum, Float, Bignum
    value + rand_number
  when String
    rand_string
  when Symbol
    rand_symbol
  when Regexp
    Regexp.new(Regexp.escape(rand_string.gsub(/\//, '\\/')))
  when Range
    rand_range
  when NilClass, FalseClass
    rand_number
  when TrueClass
    false
  else
    nil
  end
end

#rand_numberObject

Returns a random Fixnum.



286
287
288
# File 'lib/zombie-chaser/chaser.rb', line 286

def rand_number
  (rand(100) + 1)*((-1)**rand(2))
end

#rand_rangeObject

Returns a random Range



313
314
315
316
317
# File 'lib/zombie-chaser/chaser.rb', line 313

def rand_range
  min = rand(50)
  max = min + rand(50)
  min..max
end

#rand_stringObject

Returns a random String



293
294
295
296
297
298
# File 'lib/zombie-chaser/chaser.rb', line 293

def rand_string
  size = rand(50)
  str = ""
  size.times { str << rand(126).chr }
  str
end

#rand_symbolObject

Returns a random Symbol



303
304
305
306
307
308
# File 'lib/zombie-chaser/chaser.rb', line 303

def rand_symbol
  letters = ('a'..'z').to_a + ('A'..'Z').to_a
  str = ""
  (rand(50) + 1).times { str << letters[rand(letters.size)] }
  :"#{str}"
end

#record_passing_mutationObject



144
145
146
# File 'lib/zombie-chaser/chaser.rb', line 144

def record_passing_mutation
  @failure = true
end

#run_testsObject



109
110
111
112
113
114
115
# File 'lib/zombie-chaser/chaser.rb', line 109

def run_tests
  if zombie_survives? then
    record_passing_mutation
  else
    @reporter.report_test_failures
  end
end

#silence_streamObject

Suppresses output on $stdout and $stderr.



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/zombie-chaser/chaser.rb', line 322

def silence_stream
  return yield if @@debug

  begin
    dead = File.open(Chaser::NULL_PATH, "w")

    $stdout.flush
    $stderr.flush

    oldstdout = $stdout.dup
    oldstderr = $stderr.dup

    $stdout.reopen(dead)
    $stderr.reopen(dead)

    result = yield

  ensure
    $stdout.flush
    $stderr.flush

    $stdout.reopen(oldstdout)
    $stderr.reopen(oldstderr)
    result
  end
end

#tests_pass?Boolean

Overwrite test_pass? for your own Chaser runner.

Returns:

  • (Boolean)

Raises:

  • (NotImplementedError)


105
106
107
# File 'lib/zombie-chaser/chaser.rb', line 105

def tests_pass?
  raise NotImplementedError
end

#unmodify_class_methodObject



169
170
171
172
173
174
175
176
# File 'lib/zombie-chaser/chaser.rb', line 169

def unmodify_class_method
  chaser = self
  @mutated = false
  chaser_proxy_method_name = calculate_proxy_method_name(clean_method_name)
  aliasing_class(@method_name).send(:define_method, chaser_proxy_method_name) do |block, *args|
    chaser.old_method.bind(self).call(*args) {|*yielded_values| block.call(*yielded_values)}
  end
end

#unmodify_instance_methodObject



160
161
162
163
164
165
166
167
# File 'lib/zombie-chaser/chaser.rb', line 160

def unmodify_instance_method
  chaser = self
  @mutated = false
  chaser_proxy_method_name = calculate_proxy_method_name(@method_name)
  @klass.send(:define_method, chaser_proxy_method_name) do |block, *args|
    chaser.old_method.bind(self).call(*args) {|*yielded_values| block.call(*yielded_values)}
  end
end

#unmodify_methodObject



239
240
241
242
243
244
245
# File 'lib/zombie-chaser/chaser.rb', line 239

def unmodify_method
  if method_name.to_s =~ /self\./ #TODO fix duplication. Give the test a name
    unmodify_class_method
  else
    unmodify_instance_method
  end
end

#validateObject

Running the script



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/zombie-chaser/chaser.rb', line 120

def validate
  @reporter.method_loaded(klass_name, method_name)

  begin
    modify_method
    timeout(@@timeout, Chaser::Timeout) { run_tests }
  rescue Chaser::Timeout
    @reporter.warning "Your tests timed out. Chaser may have caused an infinite loop."
  rescue Interrupt
    @reporter.warning 'Mutation canceled, hit ^C again to exit'
    sleep 2
  end

  unmodify_method # in case we're validating again. we should clean up.

  if @failure
    @reporter.report_failure
    false
  else
    @reporter.no_surviving_mutant
    true
  end
end