zombie-chaser 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt ADDED
@@ -0,0 +1,47 @@
1
+ == DESCRIPTION:
2
+
3
+ Zombie chaser is a graphic(al) interface to mutation testing. Kill off the mutants, or they will eat your brains!
4
+
5
+ The human running across the screen represents the normal running of your unit tests. If one of them fails, then the human dies.
6
+
7
+ Then the zombies chase after you. Each zombie represents a mutation to your code. If your unit tests detect that something's wrong with your code, then the mutation gets killed. Otherwise, the zombie gets to meet you.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Code is slightly different to chaser.
12
+ * Not quite finished.
13
+
14
+ == REQUIREMENTS:
15
+
16
+ * Gosu
17
+ * Test-unit (for ruby 1.9)
18
+
19
+ == INSTALL:
20
+
21
+ * sudo gem install zombie-chaser
22
+
23
+ == LICENSE:
24
+
25
+ (The MIT License)
26
+
27
+ Copyright (c) 2006-2009 Ryan Davis and Kevin Clark and Andrew Grimm,
28
+ Chris Lloyd, Dave Newman, Carl Woodward & Daniel Bogan.
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining
31
+ a copy of this software and associated documentation files (the
32
+ 'Software'), to deal in the Software without restriction, including
33
+ without limitation the rights to use, copy, modify, merge, publish,
34
+ distribute, sublicense, and/or sell copies of the Software, and to
35
+ permit persons to whom the Software is furnished to do so, subject to
36
+ the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be
39
+ included in all copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
42
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
44
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
45
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
46
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
47
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ task :default => [:test]
2
+
3
+ begin
4
+ require 'jeweler'
5
+ require File.dirname(__FILE__) + "/lib/zombie_test_chaser.rb"
6
+ Jeweler::Tasks.new do |gemspec|
7
+ gemspec.name = "zombie-chaser"
8
+ gemspec.summary = "Lightweight mutation testing ... with ZOMBIES!!!"
9
+ gemspec.description = "A zombie-themed graphic(al) user interface for mutation testing"
10
+ gemspec.email = "andrew.j.grimm@gmail.com"
11
+ gemspec.authors = ["Andrew Grimm", "Ryan Davis", "Eric Hodel", "Kevin Clark"]
12
+ #gemspec.add_dependency('test-unit') #FIXME Don't know how to only add the dependency for ruby 1.9
13
+ gemspec.add_dependency('gosu') #FIXME add option for command-line version, which'll make gosu an optional dependency
14
+ gemspec.version = ZombieTestChaser::VERSION
15
+ gemspec.homepage = "http://andrewjgrimm.wordpress.com/2009/11/08/declare-war-on-everything-with-chaser/"
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ task :test do
22
+ ruby "test/test_unit.rb"
23
+ end
24
+
data/bin/zombie-chaser ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+ require 'zombie_test_chaser'
5
+ require 'optparse'
6
+
7
+ force = false
8
+ target_everything = false
9
+
10
+ opts = OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{File.basename($0)} class_name [method_name]"
12
+ opts.on("-v", "--verbose", "Loudly explain chaser run") do |opt|
13
+ ZombieTestChaser.debug = true
14
+ end
15
+
16
+ opts.on("-V", "--version", "Prints zombie-chaser's version number") do |opt|
17
+ puts "zombie-chaser #{ZombieTestChaser::VERSION}"
18
+ exit 0
19
+ end
20
+
21
+ opts.on("-t", "--tests TEST_PATTERN",
22
+ "Location of tests (glob). Unix-style even on Windows, so use forward slashes.") do |pattern|
23
+ ZombieTestChaser.test_pattern = pattern
24
+ end
25
+
26
+ opts.on("--everything", "Zombie chase all classes") do |opt|
27
+ puts "You're now facing a plague of zombies!"
28
+ target_everything = true
29
+ end
30
+
31
+ opts.on("-F", "--force", "Ignore initial test failures") do |opt|
32
+ force = true
33
+ end
34
+
35
+ opts.on("-T", "--timeout SECONDS", "The maximum time for a test run in seconds",
36
+ "Used to catch infinite loops") do |timeout|
37
+ Chaser.timeout = timeout.to_i
38
+ puts "Setting timeout at #{timeout} seconds."
39
+ end
40
+
41
+ opts.on("-r", "--random-seed SEED", "Random seed number (under development)") do |seed|
42
+ srand(seed.to_i)
43
+ end
44
+
45
+ opts.on("-h", "--help", "Show this message") do |opt|
46
+ puts opts
47
+ exit 0
48
+ end
49
+ end
50
+
51
+ looks_like_rails = test ?f, 'config/environment.rb'
52
+ ZombieTestChaser.test_pattern = "test/**/*.rb" if looks_like_rails
53
+
54
+ opts.parse!
55
+
56
+ impl = ARGV.shift
57
+ meth = ARGV.shift
58
+
59
+ unless impl or target_everything then
60
+ puts opts
61
+ exit 1
62
+ end
63
+
64
+ exit ZombieTestChaser.validate(impl, meth, force)
65
+
data/lib/chaser.rb ADDED
@@ -0,0 +1,373 @@
1
+ require 'rubygems'
2
+ require 'timeout'
3
+ require 'world'
4
+
5
+ class String # :nodoc:
6
+ def to_class
7
+ split(/::/).inject(Object) { |klass, name| klass.const_get(name) }
8
+ end
9
+ end
10
+
11
+ ##
12
+ # Test Unit Sadism
13
+
14
+ class Chaser
15
+
16
+ class Timeout < Timeout::Error; end
17
+
18
+ ##
19
+ # The version of Chaser you are using.
20
+
21
+ VERSION = '0.0.4'
22
+
23
+ ##
24
+ # Is this platform MS Windows-like?
25
+
26
+ WINDOZE = RUBY_PLATFORM =~ /mswin/
27
+
28
+ ##
29
+ # Path to the bit bucket.
30
+
31
+ NULL_PATH = WINDOZE ? 'NUL:' : '/dev/null'
32
+
33
+ ##
34
+ # Class being chased
35
+
36
+ attr_accessor :klass
37
+
38
+ ##
39
+ # Name of class being chased
40
+
41
+ attr_accessor :klass_name
42
+
43
+ ##
44
+ # Method being chased
45
+
46
+ attr_accessor :method
47
+
48
+ ##
49
+ # Name of method being chased
50
+
51
+ attr_accessor :method_name
52
+
53
+ ##
54
+ # The original version of the method being chased
55
+
56
+ attr_reader :old_method
57
+
58
+ @@debug = false
59
+ @@guess_timeout = true
60
+ @@timeout = 60 # default to something longer (can be overridden by runners)
61
+
62
+ def self.debug
63
+ @@debug
64
+ end
65
+
66
+ def self.debug=(value)
67
+ @@debug = value
68
+ end
69
+
70
+ def self.timeout=(value)
71
+ @@timeout = value
72
+ @@guess_timeout = false # We've set the timeout, don't guess
73
+ end
74
+
75
+ def self.guess_timeout?
76
+ @@guess_timeout
77
+ end
78
+
79
+ ##
80
+ # Creates a new Chaser that will chase +klass_name+ and +method_name+,
81
+ # sending results to +reporter+.
82
+
83
+ def initialize(klass_name = nil, method_name = nil, reporter = Reporter.new)
84
+ @klass_name = klass_name
85
+ @method_name = method_name.intern if method_name
86
+
87
+ @klass = klass_name.to_class if klass_name
88
+
89
+ @method = nil
90
+ @reporter = reporter
91
+
92
+ @mutated = false
93
+
94
+ @failure = false
95
+ end
96
+
97
+ ##
98
+ # Overwrite test_pass? for your own Chaser runner.
99
+
100
+ def tests_pass?
101
+ raise NotImplementedError
102
+ end
103
+
104
+ def run_tests
105
+ if zombie_survives? then
106
+ record_passing_mutation
107
+ else
108
+ @reporter.report_test_failures
109
+ end
110
+ end
111
+
112
+ ############################################################
113
+ ### Running the script
114
+
115
+ def validate
116
+ @reporter.method_loaded(klass_name, method_name)
117
+
118
+ begin
119
+ modify_method
120
+ timeout(@@timeout, Chaser::Timeout) { run_tests }
121
+ rescue Chaser::Timeout
122
+ @reporter.warning "Your tests timed out. Chaser may have caused an infinite loop."
123
+ rescue Interrupt
124
+ @reporter.warning 'Mutation canceled, hit ^C again to exit'
125
+ sleep 2
126
+ end
127
+
128
+ unmodify_method # in case we're validating again. we should clean up.
129
+
130
+ if @failure
131
+ @reporter.report_failure
132
+ false
133
+ else
134
+ @reporter.no_surviving_mutant
135
+ true
136
+ end
137
+ end
138
+
139
+ def record_passing_mutation
140
+ @failure = true
141
+ end
142
+
143
+ def calculate_proxy_method_name(original_name)
144
+ result = "__chaser_proxy__#{original_name}"
145
+ character_renaming = {"[]" => "square_brackets", "^" => "exclusive_or",
146
+ "=" => "equals", "&" => "ampersand", "*" => "splat", "+" => "plus",
147
+ "-" => "minus", "%" => "percent", "~" => "tilde", "@" => "at",
148
+ "/" => "forward_slash", "<" => "less_than", ">" => "greater_than"}
149
+ character_renaming.each do |characters, renamed_string_portion|
150
+ result.gsub!(characters, renamed_string_portion)
151
+ end
152
+ result
153
+ end
154
+
155
+ def unmodify_instance_method
156
+ chaser = self
157
+ @mutated = false
158
+ chaser_proxy_method_name = calculate_proxy_method_name(@method_name)
159
+ @klass.send(:define_method, chaser_proxy_method_name) do |block, *args|
160
+ chaser.old_method.bind(self).call(*args) {|*yielded_values| block.call(*yielded_values)}
161
+ end
162
+ end
163
+
164
+ def unmodify_class_method
165
+ chaser = self
166
+ @mutated = false
167
+ chaser_proxy_method_name = calculate_proxy_method_name(clean_method_name)
168
+ aliasing_class(@method_name).send(:define_method, chaser_proxy_method_name) do |block, *args|
169
+ chaser.old_method.bind(self).call(*args) {|*yielded_values| block.call(*yielded_values)}
170
+ end
171
+ end
172
+
173
+ # Ruby 1.8 doesn't allow define_method to handle blocks.
174
+ # The blog post http://coderrr.wordpress.com/2008/10/29/using-define_method-with-blocks-in-ruby-18/
175
+ # show that define_method has problems, and showed how to do workaround_method_code_string
176
+ def modify_instance_method
177
+ chaser = self
178
+ @mutated = true
179
+ @old_method = @klass.instance_method(@method_name)
180
+ chaser_proxy_method_name = calculate_proxy_method_name(@method_name)
181
+ workaround_method_code_string = <<-EOM
182
+ def #{@method_name}(*args, &block)
183
+ #{chaser_proxy_method_name}(block, *args)
184
+ end
185
+ EOM
186
+ @klass.class_eval do
187
+ eval(workaround_method_code_string)
188
+ end
189
+ @klass.send(:define_method, chaser_proxy_method_name) do |block, *args|
190
+ original_value = chaser.old_method.bind(self).call(*args) do |*yielded_values|
191
+ mutated_yielded_values = yielded_values.map{|value| chaser.mutate_value(value)}
192
+ block.call(*mutated_yielded_values)
193
+ end
194
+ chaser.mutate_value(original_value)
195
+ end
196
+ end
197
+
198
+ def modify_class_method
199
+ chaser = self
200
+ @mutated = true
201
+ @old_method = aliasing_class(@method_name).instance_method(clean_method_name)
202
+ chaser_proxy_method_name = calculate_proxy_method_name(clean_method_name)
203
+ workaround_method_code_string = <<-EOM
204
+ def #{@method_name}(*args, &block)
205
+ #{chaser_proxy_method_name}(block, *args)
206
+ end
207
+ EOM
208
+ @klass.class_eval do
209
+ eval(workaround_method_code_string)
210
+ end
211
+ aliasing_class(@method_name).send(:define_method, chaser_proxy_method_name) do |block, *args|
212
+ original_value = chaser.old_method.bind(self).call(*args) do |*yielded_values|
213
+ mutated_yielded_values = yielded_values.map{|value| chaser.mutate_value(value)}
214
+ block.call(*mutated_yielded_values)
215
+ end
216
+ chaser.mutate_value(original_value)
217
+ end
218
+ end
219
+
220
+ def modify_method
221
+ if method_name.to_s =~ /self\./
222
+ modify_class_method
223
+ else
224
+ modify_instance_method
225
+ end
226
+ end
227
+
228
+ def unmodify_method
229
+ if method_name.to_s =~ /self\./ #TODO fix duplication. Give the test a name
230
+ unmodify_class_method
231
+ else
232
+ unmodify_instance_method
233
+ end
234
+ end
235
+
236
+
237
+ ##
238
+ # Replaces the value with a random value.
239
+
240
+ def mutate_value(value)
241
+ case value
242
+ when Fixnum, Float, Bignum
243
+ value + rand_number
244
+ when String
245
+ rand_string
246
+ when Symbol
247
+ rand_symbol
248
+ when Regexp
249
+ Regexp.new(Regexp.escape(rand_string.gsub(/\//, '\\/')))
250
+ when Range
251
+ rand_range
252
+ when NilClass, FalseClass
253
+ rand_number
254
+ when TrueClass
255
+ false
256
+ else
257
+ nil
258
+ end
259
+ end
260
+
261
+ ############################################################
262
+ ### Convenience methods
263
+
264
+ def aliasing_class(method_name)
265
+ method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
266
+ end
267
+
268
+ def clean_method_name
269
+ method_name.to_s.gsub(/self\./, '')
270
+ end
271
+
272
+ ##
273
+ # Returns a random Fixnum.
274
+
275
+ def rand_number
276
+ (rand(100) + 1)*((-1)**rand(2))
277
+ end
278
+
279
+ ##
280
+ # Returns a random String
281
+
282
+ def rand_string
283
+ size = rand(50)
284
+ str = ""
285
+ size.times { str << rand(126).chr }
286
+ str
287
+ end
288
+
289
+ ##
290
+ # Returns a random Symbol
291
+
292
+ def rand_symbol
293
+ letters = ('a'..'z').to_a + ('A'..'Z').to_a
294
+ str = ""
295
+ (rand(50) + 1).times { str << letters[rand(letters.size)] }
296
+ :"#{str}"
297
+ end
298
+
299
+ ##
300
+ # Returns a random Range
301
+
302
+ def rand_range
303
+ min = rand(50)
304
+ max = min + rand(50)
305
+ min..max
306
+ end
307
+
308
+ ##
309
+ # Suppresses output on $stdout and $stderr.
310
+
311
+ def silence_stream
312
+ return yield if @@debug
313
+
314
+ begin
315
+ dead = File.open(Chaser::NULL_PATH, "w")
316
+
317
+ $stdout.flush
318
+ $stderr.flush
319
+
320
+ oldstdout = $stdout.dup
321
+ oldstderr = $stderr.dup
322
+
323
+ $stdout.reopen(dead)
324
+ $stderr.reopen(dead)
325
+
326
+ result = yield
327
+
328
+ ensure
329
+ $stdout.flush
330
+ $stderr.flush
331
+
332
+ $stdout.reopen(oldstdout)
333
+ $stderr.reopen(oldstderr)
334
+ result
335
+ end
336
+ end
337
+
338
+ class Reporter
339
+ def method_loaded(klass_name, method_name)
340
+ info "#{klass_name}\##{method_name} loaded"
341
+ end
342
+
343
+ def warning(message)
344
+ puts "!" * 70
345
+ puts "!!! #{message}"
346
+ puts "!" * 70
347
+ puts
348
+ end
349
+
350
+ def info(message)
351
+ puts "*"*70
352
+ puts "*** #{message}"
353
+ puts "*"*70
354
+ puts
355
+ end
356
+
357
+ def report_failure
358
+ puts
359
+ puts "The affected method didn't cause test failures."
360
+ puts
361
+ end
362
+
363
+ def no_surviving_mutant
364
+ puts "The mutant didn't survive. Cool!\n\n"
365
+ end
366
+
367
+ def report_test_failures
368
+ puts "Tests failed -- this is good" if Chaser.debug
369
+ end
370
+ end
371
+
372
+ end
373
+
data/lib/human.rb ADDED
@@ -0,0 +1,180 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. ui])
2
+
3
+ require "test_unit_handler"
4
+ require "ui" #For actor superclass
5
+
6
+ class Human < Actor
7
+ private_class_method :new
8
+ attr_reader :successful_step_count
9
+
10
+ def self.new_using_test_unit_handler(test_pattern, world)
11
+ new(test_pattern, world)
12
+ end
13
+
14
+ def initialize(test_pattern, world)
15
+ @status = nil #Currently only used by zombie
16
+ @world = world
17
+ @successful_step_count = 0
18
+ @health = :alive
19
+ @test_handler = TestUnitHandler.new(test_pattern, self)
20
+ end
21
+
22
+ def run
23
+ notify_world
24
+ @test_handler.run
25
+ end
26
+
27
+ def current_symbol
28
+ case @health
29
+ when :alive
30
+ "@"
31
+ when :dying
32
+ "*"
33
+ when :dead
34
+ "+"
35
+ end
36
+ end
37
+
38
+ def actor_type
39
+ 'robot'
40
+ end
41
+
42
+ def actor_state
43
+ return "attacking" if @status == :attacking
44
+ case @health
45
+ when :alive
46
+ "moving"
47
+ when :dying
48
+ "dying"
49
+ when :dead
50
+ "dead"
51
+ end
52
+ end
53
+
54
+ def actor_direction
55
+ 270.0
56
+ end
57
+
58
+ def notify_passing_step
59
+ @successful_step_count += 1
60
+ notify_world
61
+ end
62
+
63
+ def notify_failing_step
64
+ @health = :dying
65
+ notify_world
66
+ end
67
+
68
+ def dying?
69
+ @health == :dying
70
+ end
71
+
72
+ def dead?
73
+ @health == :dead
74
+ end
75
+
76
+ def finish_dying
77
+ sleep 0.5
78
+ raise "I'm not dead yet!" unless dying?
79
+ @health = :dead
80
+ notify_world
81
+ sleep 0.5
82
+ end
83
+
84
+ def notify_world
85
+ @world.something_happened
86
+ end
87
+
88
+ def get_eaten
89
+ @health = :dying unless dead?
90
+ end
91
+ end
92
+
93
+ class MockHuman < Human
94
+ private_class_method :new
95
+
96
+ def self.new_using_results(results, world)
97
+ new(results, world)
98
+ end
99
+
100
+ def initialize(results, world)
101
+ @world = world
102
+ @results = results
103
+ @successful_step_count = 0
104
+ @health = :alive
105
+ end
106
+
107
+ def run
108
+ until @successful_step_count == @results.size
109
+ if @results[@successful_step_count] == :failure
110
+ @health = :dying
111
+ notify_world
112
+ return
113
+ end
114
+ notify_world
115
+ @successful_step_count += 1
116
+ end
117
+ notify_world
118
+ end
119
+ end
120
+
121
+ class MockZombieList
122
+
123
+ def self.new_using_results(zombies_results, world)
124
+ zombies = zombies_results.map do |zombie_results|
125
+ MockZombie.new_using_results(zombie_results, world)
126
+ end
127
+ new(zombies)
128
+ end
129
+
130
+ def initialize(zombies)
131
+ @zombies = zombies
132
+ @current_zombie_number = 0
133
+ end
134
+
135
+ def supply_next_zombie
136
+ zombie = @zombies[@current_zombie_number]
137
+ @current_zombie_number += 1
138
+ zombie
139
+ end
140
+
141
+ def all_slain?
142
+ @current_zombie_number == @zombies.length
143
+ end
144
+ end
145
+
146
+ module ZombieInterface
147
+ def current_symbol
148
+ case @health
149
+ when :alive
150
+ "Z"
151
+ when :dying
152
+ "*"
153
+ when :dead
154
+ "+"
155
+ end
156
+ end
157
+
158
+ def actor_type
159
+ 'zombie'
160
+ end
161
+
162
+ def actor_direction
163
+ 90.0
164
+ end
165
+
166
+ end
167
+
168
+ class Zombie < Human
169
+ include ZombieInterface
170
+
171
+ def eat(human)
172
+ @status = :attacking #Even if the human's dead, look for leftovers
173
+ human.get_eaten
174
+ end
175
+ end
176
+
177
+ class MockZombie < MockHuman #Fixme provide a proper hierarchy
178
+ include ZombieInterface
179
+
180
+ end