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.
- data/History.txt +22 -0
- data/README.txt +57 -47
- data/Rakefile +26 -24
- data/bin/zombie-chaser +77 -79
- data/lib/{chaser.rb → zombie-chaser/chaser.rb} +392 -373
- data/lib/zombie-chaser/human.rb +312 -0
- data/{ui → lib/zombie-chaser}/icons/death.png +0 -0
- data/{ui → lib/zombie-chaser}/icons/robot.png +0 -0
- data/lib/zombie-chaser/interface.rb +153 -0
- data/{ui → lib/zombie-chaser}/sprites/robot-attacking.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/robot-dead.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/robot-dying.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/robot-idle.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/robot-moving.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/robot-turning.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/tank-attacking.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/tank-dead.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/tank-idle.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/tank-moving.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/tank-turning.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/witch-attacking.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/witch-dead.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/witch-idle.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/witch-moving.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/witch-turning.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/zombie-attacking.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/zombie-dead.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/zombie-dying.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/zombie-idle.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/zombie-moving.png +0 -0
- data/{ui → lib/zombie-chaser}/sprites/zombie-turning.png +0 -0
- data/lib/zombie-chaser/test_unit_handler.rb +78 -0
- data/{ui → lib/zombie-chaser}/tiles/grass.png +0 -0
- data/{ui → lib/zombie-chaser}/tiles/shrubbery.png +0 -0
- data/{ui → lib/zombie-chaser}/ui.rb +165 -127
- data/lib/{world.rb → zombie-chaser/world.rb} +105 -98
- data/lib/zombie-chaser/zombie_test_chaser.rb +139 -0
- data/test/fixtures/chased.rb +56 -56
- data/test/integration.rb +58 -0
- data/test/test_chaser.rb +150 -144
- data/test/test_unit.rb +2 -2
- data/test/test_zombie.rb +302 -108
- data/zombie-chaser.gemspec +88 -88
- metadata +40 -46
- data/lib/human.rb +0 -189
- data/lib/interface.rb +0 -86
- data/lib/test_unit_handler.rb +0 -43
- 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
|
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
|
-
|
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
|
-
|
6
|
-
|
7
|
-
gemspec.
|
8
|
-
gemspec.
|
9
|
-
gemspec.
|
10
|
-
gemspec.
|
11
|
-
gemspec.
|
12
|
-
#
|
13
|
-
gemspec.add_dependency('gosu') #
|
14
|
-
|
15
|
-
gemspec.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
-
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
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
end
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
def
|
276
|
-
|
277
|
-
end
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
350
|
-
def
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
+
|