zombie-chaser 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/History.txt +22 -0
  2. data/README.txt +57 -47
  3. data/Rakefile +26 -24
  4. data/bin/zombie-chaser +77 -79
  5. data/lib/{chaser.rb → zombie-chaser/chaser.rb} +392 -373
  6. data/lib/zombie-chaser/human.rb +312 -0
  7. data/{ui → lib/zombie-chaser}/icons/death.png +0 -0
  8. data/{ui → lib/zombie-chaser}/icons/robot.png +0 -0
  9. data/lib/zombie-chaser/interface.rb +153 -0
  10. data/{ui → lib/zombie-chaser}/sprites/robot-attacking.png +0 -0
  11. data/{ui → lib/zombie-chaser}/sprites/robot-dead.png +0 -0
  12. data/{ui → lib/zombie-chaser}/sprites/robot-dying.png +0 -0
  13. data/{ui → lib/zombie-chaser}/sprites/robot-idle.png +0 -0
  14. data/{ui → lib/zombie-chaser}/sprites/robot-moving.png +0 -0
  15. data/{ui → lib/zombie-chaser}/sprites/robot-turning.png +0 -0
  16. data/{ui → lib/zombie-chaser}/sprites/tank-attacking.png +0 -0
  17. data/{ui → lib/zombie-chaser}/sprites/tank-dead.png +0 -0
  18. data/{ui → lib/zombie-chaser}/sprites/tank-idle.png +0 -0
  19. data/{ui → lib/zombie-chaser}/sprites/tank-moving.png +0 -0
  20. data/{ui → lib/zombie-chaser}/sprites/tank-turning.png +0 -0
  21. data/{ui → lib/zombie-chaser}/sprites/witch-attacking.png +0 -0
  22. data/{ui → lib/zombie-chaser}/sprites/witch-dead.png +0 -0
  23. data/{ui → lib/zombie-chaser}/sprites/witch-idle.png +0 -0
  24. data/{ui → lib/zombie-chaser}/sprites/witch-moving.png +0 -0
  25. data/{ui → lib/zombie-chaser}/sprites/witch-turning.png +0 -0
  26. data/{ui → lib/zombie-chaser}/sprites/zombie-attacking.png +0 -0
  27. data/{ui → lib/zombie-chaser}/sprites/zombie-dead.png +0 -0
  28. data/{ui → lib/zombie-chaser}/sprites/zombie-dying.png +0 -0
  29. data/{ui → lib/zombie-chaser}/sprites/zombie-idle.png +0 -0
  30. data/{ui → lib/zombie-chaser}/sprites/zombie-moving.png +0 -0
  31. data/{ui → lib/zombie-chaser}/sprites/zombie-turning.png +0 -0
  32. data/lib/zombie-chaser/test_unit_handler.rb +78 -0
  33. data/{ui → lib/zombie-chaser}/tiles/grass.png +0 -0
  34. data/{ui → lib/zombie-chaser}/tiles/shrubbery.png +0 -0
  35. data/{ui → lib/zombie-chaser}/ui.rb +165 -127
  36. data/lib/{world.rb → zombie-chaser/world.rb} +105 -98
  37. data/lib/zombie-chaser/zombie_test_chaser.rb +139 -0
  38. data/test/fixtures/chased.rb +56 -56
  39. data/test/integration.rb +58 -0
  40. data/test/test_chaser.rb +150 -144
  41. data/test/test_unit.rb +2 -2
  42. data/test/test_zombie.rb +302 -108
  43. data/zombie-chaser.gemspec +88 -88
  44. metadata +40 -46
  45. data/lib/human.rb +0 -189
  46. data/lib/interface.rb +0 -86
  47. data/lib/test_unit_handler.rb +0 -43
  48. data/lib/zombie_test_chaser.rb +0 -133
data/History.txt ADDED
@@ -0,0 +1,22 @@
1
+ === 0.1.0 / 2010-04-11
2
+
3
+ * Zombie speed is inversely proportional to number of tests, so numerous tests won't take to long.
4
+ * Console based interface width is configurable.
5
+ * There are multiple zombies, and can come from all directions.
6
+ * Zombies lurch as they move.
7
+ * Fixed nethack-style representations and messages being on the same line when using console based interface.
8
+ * Gosu has to be installed manually, as it's not listed as a dependency.
9
+
10
+ == 0.0.3 / 2009-12-13 7274dd9eeb09720a6be2afc5a261c878c2dfc7a1
11
+
12
+ * GUI window size is configurable.
13
+ * Users can choose between GUI and console based interface.
14
+ * Ensured that excessive tests won't cause images to go off the screen.
15
+
16
+ == 0.0.2 / 2009-12-08 0a888b2ea33481e078ab7640c9fd38dc6a23710f
17
+
18
+ * Fix of brown paper bag bug: images weren't found when in gem form.
19
+
20
+ == 0.0.1 / 2009-12-07 4881d00aaa9e19491b191a5c35b351bffdd08212
21
+
22
+ * Initial release
data/README.txt CHANGED
@@ -1,47 +1,57 @@
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.
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 (your code doing something it shouldn't be doing). If your unit tests detect that something's wrong with your code, then the mutation gets killed. Otherwise, the zombie gets to meat you.
8
+
9
+ There are two alternatives for the interface. One is a GUI, while the other is a nethack-style interface that runs within the console itself. Zombie-chaser aims to be compatible with any flavor of ruby on any platform.
10
+
11
+ == REQUIREMENTS:
12
+
13
+ * Gosu (optional)
14
+ * Test-unit (for ruby 1.9)
15
+
16
+ == INSTALL:
17
+
18
+ * [sudo] gem install zombie-chaser
19
+ * [sudo] gem install gosu #Optional
20
+ * [sudo] gem install test-unit #Optional
21
+
22
+ * Don't use sudo if it's not applicable (can't or don't want to use root, or you're using Windows)
23
+ * Gosu is not listed as a dependency, as otherwise jruby complains about gosu's absence before the program starts. Therefore you have to install it manually if you want a GUI interface.
24
+ * If you're using ruby 1.9, you'll need to install the gem version of test-unit.
25
+
26
+ == CURRENT BUGS:
27
+
28
+ * Resource-intensive tests are especially slow in GUI mode for some reason. Run them in console mode (using --console) to make the program run faster.
29
+ * Very occasionally, the program can crash, possibly because of threading issues. Please notify me if it becomes a consistent problem.
30
+ * GUI version doesn't work in ruby 1.9.
31
+ * Neither GUI nor console works on jruby for ruby version 1.9.
32
+
33
+ == LICENSE:
34
+
35
+ (The MIT License)
36
+
37
+ Copyright (c) 2006-2009 Ryan Davis and Kevin Clark and Andrew Grimm,
38
+ Chris Lloyd, Dave Newman, Carl Woodward & Daniel Bogan.
39
+
40
+ Permission is hereby granted, free of charge, to any person obtaining
41
+ a copy of this software and associated documentation files (the
42
+ 'Software'), to deal in the Software without restriction, including
43
+ without limitation the rights to use, copy, modify, merge, publish,
44
+ distribute, sublicense, and/or sell copies of the Software, and to
45
+ permit persons to whom the Software is furnished to do so, subject to
46
+ the following conditions:
47
+
48
+ The above copyright notice and this permission notice shall be
49
+ included in all copies or substantial portions of the Software.
50
+
51
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
52
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
53
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
54
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
55
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
56
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
57
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -1,24 +1,26 @@
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
-
1
+ task :default => [:test]
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "zombie-chaser"
7
+ gemspec.summary = "Lightweight mutation testing ... with ZOMBIES!!!"
8
+ gemspec.description = "A zombie-themed graphic(al) user interface for mutation testing"
9
+ gemspec.email = "andrew.j.grimm@gmail.com"
10
+ gemspec.authors = ["Andrew Grimm", "Ryan Davis", "Eric Hodel", "Kevin Clark"]
11
+ #gemspec.add_dependency('test-unit') #FIXME Don't know how to only add the dependency for ruby 1.9
12
+ # Question about conditional dependency asked at http://stackoverflow.com/questions/1620342/how-do-i-add-conditional-rubygem-requirements-to-a-gem-specification
13
+ #gemspec.add_dependency('gosu') #TODO ask rubygems people to make it possible to specify an optional runtime dependency
14
+ # Comment about optional runtime dependencies at https://oree.ch/2009/06/06/rubygems-dependencies.html argues you shouldn't list optional dependencies
15
+ #gemspec.version = ZombieTestChaser::VERSION #Can't access ZombieTestChaser without starting up test/unit
16
+ gemspec.version = '0.1.0' # Check that it's consistent with ZombieTestChaser::VERSION
17
+ gemspec.homepage = "http://andrewjgrimm.wordpress.com/2009/11/08/declare-war-on-everything-with-chaser/"
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
21
+ end
22
+
23
+ #Fixme work out how to change this to TestTask without problems
24
+ task :test do
25
+ ruby "-Ilib", "-Itest", "test/test_unit.rb"
26
+ end
data/bin/zombie-chaser CHANGED
@@ -1,79 +1,77 @@
1
- #!/usr/local/bin/ruby
2
-
3
- $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
- $:.unshift File.join(File.dirname(__FILE__), *%w[.. ui])
5
- require 'zombie_test_chaser'
6
- require 'ui'
7
- require 'optparse'
8
-
9
- force = false
10
- target_everything = false
11
-
12
- opts = OptionParser.new do |opts|
13
- opts.banner = "Usage: #{File.basename($0)} class_name [method_name]"
14
- opts.on("-v", "--verbose", "Loudly explain chaser run") do |opt|
15
- ZombieTestChaser.debug = true
16
- end
17
-
18
- opts.on("-V", "--version", "Prints zombie-chaser's version number") do |opt|
19
- puts "zombie-chaser #{ZombieTestChaser::VERSION}"
20
- exit 0
21
- end
22
-
23
- opts.on("-t", "--tests TEST_PATTERN",
24
- "Location of tests (glob). Unix-style even on Windows, so use forward slashes.") do |pattern|
25
- ZombieTestChaser.test_pattern = pattern
26
- end
27
-
28
- opts.on("--everything", "Zombie chase all classes") do |opt|
29
- puts "You're now facing a plague of zombies!"
30
- target_everything = true
31
- end
32
-
33
- opts.on("-F", "--force", "Ignore initial test failures") do |opt|
34
- force = true
35
- end
36
-
37
- opts.on("-T", "--timeout SECONDS", "The maximum time for a test run in seconds",
38
- "Used to catch infinite loops") do |timeout|
39
- Chaser.timeout = timeout.to_i
40
- puts "Setting timeout at #{timeout} seconds."
41
- end
42
-
43
- opts.on("--width PIXELS", "Width of screen in pixels") do |width|
44
- Window.width = Integer(width)
45
- end
46
-
47
- opts.on("--height PIXELS", "Height of screen in pixels") do |height|
48
- Window.height = Integer(height)
49
- end
50
-
51
- opts.on("--console", "Use nethack-style text interface") do |opt|
52
- World.interface_type = :console_interface
53
- end
54
-
55
- opts.on("-r", "--random-seed SEED", "Random seed number (under development)") do |seed|
56
- srand(seed.to_i)
57
- end
58
-
59
- opts.on("-h", "--help", "Show this message") do |opt|
60
- puts opts
61
- exit 0
62
- end
63
- end
64
-
65
- looks_like_rails = test ?f, 'config/environment.rb'
66
- ZombieTestChaser.test_pattern = "test/**/*.rb" if looks_like_rails
67
-
68
- opts.parse!
69
-
70
- impl = ARGV.shift
71
- meth = ARGV.shift
72
-
73
- unless impl or target_everything then
74
- puts opts
75
- exit 1
76
- end
77
-
78
- exit ZombieTestChaser.validate(impl, meth, force)
79
-
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'zombie-chaser/zombie_test_chaser'
4
+ require 'optparse'
5
+
6
+ force = false
7
+ target_everything = false
8
+
9
+ opts = OptionParser.new do |opts|
10
+ opts.banner = "Usage: #{File.basename($0)} class_name [method_name]"
11
+ opts.on("-v", "--verbose", "Loudly explain chaser run") do |opt|
12
+ ZombieTestChaser.debug = true
13
+ end
14
+
15
+ opts.on("-V", "--version", "Prints zombie-chaser's version number") do |opt|
16
+ puts "zombie-chaser #{ZombieTestChaser::VERSION}"
17
+ exit 0
18
+ end
19
+
20
+ opts.on("-t", "--tests TEST_PATTERN",
21
+ "Location of tests (glob). Unix-style even on Windows, so use forward slashes.") do |pattern|
22
+ ZombieTestChaser.test_pattern = pattern
23
+ end
24
+
25
+ opts.on("--everything", "Zombie chase all classes") do |opt|
26
+ puts "You're now facing a plague of zombies!"
27
+ target_everything = true
28
+ end
29
+
30
+ opts.on("-F", "--force", "Ignore initial test failures") do |opt|
31
+ force = true
32
+ end
33
+
34
+ opts.on("-T", "--timeout SECONDS", "The maximum time for a test run in seconds",
35
+ "Used to catch infinite loops") do |timeout|
36
+ Chaser.timeout = timeout.to_i
37
+ puts "Setting timeout at #{timeout} seconds."
38
+ end
39
+
40
+ opts.on("--width PIXELS", "Width of screen in pixels (or that of a console interface in characters)") do |width|
41
+ Window.width = Integer(width)
42
+ ConsoleInterface.width = Integer(width)
43
+ end
44
+
45
+ opts.on("--height PIXELS", "Height of screen in pixels") do |height|
46
+ Window.height = Integer(height)
47
+ end
48
+
49
+ opts.on("--console", "Use nethack-style text interface") do |opt|
50
+ World.interface_type = :console_interface
51
+ end
52
+
53
+ opts.on("-r", "--random-seed SEED", "Random seed number (under development)") do |seed|
54
+ srand(seed.to_i)
55
+ end
56
+
57
+ opts.on("-h", "--help", "Show this message") do |opt|
58
+ puts opts
59
+ exit 0
60
+ end
61
+ end
62
+
63
+ looks_like_rails = test ?f, 'config/environment.rb'
64
+ ZombieTestChaser.test_pattern = "test/**/*.rb" if looks_like_rails
65
+
66
+ opts.parse!
67
+
68
+ impl = ARGV.shift
69
+ meth = ARGV.shift
70
+
71
+ unless impl or target_everything then
72
+ puts opts
73
+ exit 1
74
+ end
75
+
76
+ exit ZombieTestChaser.validate(impl, meth, force)
77
+
@@ -1,373 +1,392 @@
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
-
1
+ require 'rubygems'
2
+ require 'timeout'
3
+ require 'zombie-chaser/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
+ #For the benefit of puts calls in ZombieTestChaser.validate
80
+ def interface_puts(*args)
81
+ @reporter.interface_puts(*args)
82
+ end
83
+
84
+ ##
85
+ # Creates a new Chaser that will chase +klass_name+ and +method_name+,
86
+ # sending results to +reporter+.
87
+
88
+ def initialize(klass_name = nil, method_name = nil, reporter = Reporter.new)
89
+ @klass_name = klass_name
90
+ @method_name = method_name.intern if method_name
91
+
92
+ @klass = klass_name.to_class if klass_name
93
+
94
+ @method = nil
95
+ @reporter = reporter
96
+
97
+ @mutated = false
98
+
99
+ @failure = false
100
+ end
101
+
102
+ ##
103
+ # Overwrite test_pass? for your own Chaser runner.
104
+
105
+ def tests_pass?
106
+ raise NotImplementedError
107
+ end
108
+
109
+ def run_tests
110
+ if zombie_survives? then
111
+ record_passing_mutation
112
+ else
113
+ @reporter.report_test_failures
114
+ end
115
+ end
116
+
117
+ ############################################################
118
+ ### Running the script
119
+
120
+ def validate
121
+ @reporter.method_loaded(klass_name, method_name)
122
+
123
+ begin
124
+ modify_method
125
+ timeout(@@timeout, Chaser::Timeout) { run_tests }
126
+ rescue Chaser::Timeout
127
+ @reporter.warning "Your tests timed out. Chaser may have caused an infinite loop."
128
+ rescue Interrupt
129
+ @reporter.warning 'Mutation canceled, hit ^C again to exit'
130
+ sleep 2
131
+ end
132
+
133
+ unmodify_method # in case we're validating again. we should clean up.
134
+
135
+ if @failure
136
+ @reporter.report_failure
137
+ false
138
+ else
139
+ @reporter.no_surviving_mutant
140
+ true
141
+ end
142
+ end
143
+
144
+ def record_passing_mutation
145
+ @failure = true
146
+ end
147
+
148
+ def calculate_proxy_method_name(original_name)
149
+ result = "__chaser_proxy__#{original_name}"
150
+ character_renaming = {"[]" => "square_brackets", "^" => "exclusive_or",
151
+ "=" => "equals", "&" => "ampersand", "*" => "splat", "+" => "plus",
152
+ "-" => "minus", "%" => "percent", "~" => "tilde", "@" => "at",
153
+ "/" => "forward_slash", "<" => "less_than", ">" => "greater_than"}
154
+ character_renaming.each do |characters, renamed_string_portion|
155
+ result.gsub!(characters, renamed_string_portion)
156
+ end
157
+ result
158
+ end
159
+
160
+ def unmodify_instance_method
161
+ chaser = self
162
+ @mutated = false
163
+ chaser_proxy_method_name = calculate_proxy_method_name(@method_name)
164
+ @klass.send(:define_method, chaser_proxy_method_name) do |block, *args|
165
+ chaser.old_method.bind(self).call(*args) {|*yielded_values| block.call(*yielded_values)}
166
+ end
167
+ end
168
+
169
+ def unmodify_class_method
170
+ chaser = self
171
+ @mutated = false
172
+ chaser_proxy_method_name = calculate_proxy_method_name(clean_method_name)
173
+ aliasing_class(@method_name).send(:define_method, chaser_proxy_method_name) do |block, *args|
174
+ chaser.old_method.bind(self).call(*args) {|*yielded_values| block.call(*yielded_values)}
175
+ end
176
+ end
177
+
178
+ # Ruby 1.8 doesn't allow define_method to handle blocks.
179
+ # The blog post http://coderrr.wordpress.com/2008/10/29/using-define_method-with-blocks-in-ruby-18/
180
+ # show that define_method has problems, and showed how to do workaround_method_code_string
181
+ def modify_instance_method
182
+ chaser = self
183
+ @mutated = true
184
+ @old_method = @klass.instance_method(@method_name)
185
+ chaser_proxy_method_name = calculate_proxy_method_name(@method_name)
186
+ workaround_method_code_string = <<-EOM
187
+ def #{@method_name}(*args, &block)
188
+ #{chaser_proxy_method_name}(block, *args)
189
+ end
190
+ EOM
191
+ @klass.class_eval do
192
+ remove_method(chaser.clean_method_name.to_sym)
193
+ eval(workaround_method_code_string)
194
+ end
195
+ @klass.send(:define_method, chaser_proxy_method_name) do |block, *args|
196
+ original_value = chaser.old_method.bind(self).call(*args) do |*yielded_values|
197
+ mutated_yielded_values = yielded_values.map{|value| chaser.mutate_value(value)}
198
+ block.call(*mutated_yielded_values)
199
+ end
200
+ chaser.mutate_value(original_value)
201
+ end
202
+ end
203
+
204
+ def modify_class_method
205
+ chaser = self
206
+ @mutated = true
207
+ @old_method = aliasing_class(@method_name).instance_method(clean_method_name)
208
+
209
+ aliasing_class(@method_name).class_eval do
210
+ remove_method(chaser.clean_method_name.to_sym)
211
+ end
212
+
213
+ chaser_proxy_method_name = calculate_proxy_method_name(clean_method_name)
214
+ workaround_method_code_string = <<-EOM
215
+ def #{@method_name}(*args, &block)
216
+ #{chaser_proxy_method_name}(block, *args)
217
+ end
218
+ EOM
219
+ @klass.class_eval do
220
+ eval(workaround_method_code_string)
221
+ end
222
+ aliasing_class(@method_name).send(:define_method, chaser_proxy_method_name) do |block, *args|
223
+ original_value = chaser.old_method.bind(self).call(*args) do |*yielded_values|
224
+ mutated_yielded_values = yielded_values.map{|value| chaser.mutate_value(value)}
225
+ block.call(*mutated_yielded_values)
226
+ end
227
+ chaser.mutate_value(original_value)
228
+ end
229
+ end
230
+
231
+ def modify_method
232
+ if method_name.to_s =~ /self\./
233
+ modify_class_method
234
+ else
235
+ modify_instance_method
236
+ end
237
+ end
238
+
239
+ def unmodify_method
240
+ if method_name.to_s =~ /self\./ #TODO fix duplication. Give the test a name
241
+ unmodify_class_method
242
+ else
243
+ unmodify_instance_method
244
+ end
245
+ end
246
+
247
+
248
+ ##
249
+ # Replaces the value with a random value.
250
+
251
+ def mutate_value(value)
252
+ case value
253
+ when Fixnum, Float, Bignum
254
+ value + rand_number
255
+ when String
256
+ rand_string
257
+ when Symbol
258
+ rand_symbol
259
+ when Regexp
260
+ Regexp.new(Regexp.escape(rand_string.gsub(/\//, '\\/')))
261
+ when Range
262
+ rand_range
263
+ when NilClass, FalseClass
264
+ rand_number
265
+ when TrueClass
266
+ false
267
+ else
268
+ nil
269
+ end
270
+ end
271
+
272
+ ############################################################
273
+ ### Convenience methods
274
+
275
+ def aliasing_class(method_name)
276
+ method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
277
+ end
278
+
279
+ def clean_method_name
280
+ method_name.to_s.gsub(/self\./, '')
281
+ end
282
+
283
+ ##
284
+ # Returns a random Fixnum.
285
+
286
+ def rand_number
287
+ (rand(100) + 1)*((-1)**rand(2))
288
+ end
289
+
290
+ ##
291
+ # Returns a random String
292
+
293
+ def rand_string
294
+ size = rand(50)
295
+ str = ""
296
+ size.times { str << rand(126).chr }
297
+ str
298
+ end
299
+
300
+ ##
301
+ # Returns a random Symbol
302
+
303
+ def rand_symbol
304
+ letters = ('a'..'z').to_a + ('A'..'Z').to_a
305
+ str = ""
306
+ (rand(50) + 1).times { str << letters[rand(letters.size)] }
307
+ :"#{str}"
308
+ end
309
+
310
+ ##
311
+ # Returns a random Range
312
+
313
+ def rand_range
314
+ min = rand(50)
315
+ max = min + rand(50)
316
+ min..max
317
+ end
318
+
319
+ ##
320
+ # Suppresses output on $stdout and $stderr.
321
+
322
+ def silence_stream
323
+ return yield if @@debug
324
+
325
+ begin
326
+ dead = File.open(Chaser::NULL_PATH, "w")
327
+
328
+ $stdout.flush
329
+ $stderr.flush
330
+
331
+ oldstdout = $stdout.dup
332
+ oldstderr = $stderr.dup
333
+
334
+ $stdout.reopen(dead)
335
+ $stderr.reopen(dead)
336
+
337
+ result = yield
338
+
339
+ ensure
340
+ $stdout.flush
341
+ $stderr.flush
342
+
343
+ $stdout.reopen(oldstdout)
344
+ $stderr.reopen(oldstderr)
345
+ result
346
+ end
347
+ end
348
+
349
+ class Reporter
350
+ def initialize(interface)
351
+ @interface = interface
352
+ end
353
+
354
+ def method_loaded(klass_name, method_name)
355
+ info "#{klass_name}\##{method_name} loaded"
356
+ end
357
+
358
+ def warning(message)
359
+ interface_puts "!" * 70,
360
+ "!!! #{message}",
361
+ "!" * 70,
362
+ ""
363
+ end
364
+
365
+ def info(message)
366
+ interface_puts "*"*70,
367
+ "*** #{message}",
368
+ "*"*70,
369
+ ""
370
+ end
371
+
372
+ def report_failure
373
+ interface_puts "",
374
+ "The affected method didn't cause test failures.",
375
+ ""
376
+ end
377
+
378
+ def no_surviving_mutant
379
+ interface_puts "The mutant didn't survive. Cool!\n\n"
380
+ end
381
+
382
+ def report_test_failures
383
+ interface_puts "Tests failed -- this is good" if Chaser.debug
384
+ end
385
+
386
+ def interface_puts(*args)
387
+ @interface.interface_puts(*args)
388
+ end
389
+ end
390
+
391
+ end
392
+