zombie-chaser 0.0.1

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.
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