heckle 1.1.1 → 1.2.0
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/History.txt +12 -0
- data/Manifest.txt +0 -2
- data/bin/heckle +21 -2
- data/lib/heckle.rb +441 -2
- data/lib/test_unit_heckler.rb +30 -20
- data/sample/lib/heckled.rb +13 -0
- data/sample/test/test_heckled.rb +8 -0
- data/test/fixtures/heckled.rb +21 -17
- data/test/test_heckle.rb +221 -466
- metadata +7 -9
- data/lib/heckle/base.rb +0 -352
- data/lib/heckle/reporter.rb +0 -43
data/History.txt
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
== 1.2.0 / 2007-01-15
|
2
|
+
|
3
|
+
* 2 major enhancements:
|
4
|
+
* Timeout for tests set dynamically and overridable with -T
|
5
|
+
* Class method support with "self.method_name"
|
6
|
+
* 3 minor enhancements:
|
7
|
+
* -b allows heckling of branches only
|
8
|
+
* Restructured class heirarchy and got rid of Base and others.
|
9
|
+
* Revamped the tests and reduced size by 60%.
|
10
|
+
* 1 bug fix:
|
11
|
+
* Fixed the infinite loop caused by syntax errors
|
12
|
+
|
1
13
|
== 1.1.1 / 2006-12-20
|
2
14
|
|
3
15
|
* 3 bug fixes:
|
data/Manifest.txt
CHANGED
data/bin/heckle
CHANGED
@@ -10,11 +10,31 @@ opts = OptionParser.new do |opts|
|
|
10
10
|
TestUnitHeckler.debug = true
|
11
11
|
end
|
12
12
|
|
13
|
-
opts.on( "-
|
13
|
+
opts.on( "-V", "--version", "Prints Heckle's version number") do |opt|
|
14
|
+
puts "Heckle #{Heckle::VERSION}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on( "-t", "--tests TEST_PATTERN",
|
14
19
|
"Location of tests (glob)" ) do |pattern|
|
15
20
|
TestUnitHeckler.test_pattern = pattern
|
16
21
|
end
|
22
|
+
|
23
|
+
opts.on( "-b", "--branches-only", "Only mutate branches" ) do |opt|
|
24
|
+
puts "!"*70
|
25
|
+
puts "!!! Heckling branches only"
|
26
|
+
puts "!"*70
|
27
|
+
puts
|
28
|
+
|
29
|
+
Heckle::MUTATABLE_NODES.replace [:if, :while, :until]
|
30
|
+
end
|
17
31
|
|
32
|
+
opts.on( "-T", "--timeout SECONDS", "The maximum time for a test run in seconds",
|
33
|
+
"Used to catch infinite loops") do |timeout|
|
34
|
+
Heckle.timeout = timeout.to_i
|
35
|
+
puts "Setting timeout at #{timeout} seconds."
|
36
|
+
end
|
37
|
+
|
18
38
|
opts.on( "-h", "--help", "Show this message") do |opt|
|
19
39
|
puts opts
|
20
40
|
exit 0
|
@@ -32,4 +52,3 @@ unless impl then
|
|
32
52
|
end
|
33
53
|
|
34
54
|
TestUnitHeckler.validate(impl, meth)
|
35
|
-
|
data/lib/heckle.rb
CHANGED
@@ -1,3 +1,442 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require '
|
3
|
-
require '
|
2
|
+
require 'parse_tree'
|
3
|
+
require 'ruby2ruby'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
class String
|
7
|
+
def to_class
|
8
|
+
split(/::/).inject(Object) { |klass, name| klass.const_get(name) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Heckle < SexpProcessor
|
13
|
+
VERSION = '1.2.0'
|
14
|
+
MUTATABLE_NODES = [:if, :lit, :str, :true, :false, :while, :until]
|
15
|
+
WINDOZE = RUBY_PLATFORM =~ /mswin/
|
16
|
+
NULL_PATH = WINDOZE ? 'NUL:' : '/dev/null'
|
17
|
+
|
18
|
+
attr_accessor(:klass_name, :method_name, :klass, :method, :mutatees,
|
19
|
+
:original_tree, :mutation_count, :node_count,
|
20
|
+
:failures, :count)
|
21
|
+
|
22
|
+
@@debug = false
|
23
|
+
@@guess_timeout = true
|
24
|
+
@@timeout = 60 # default to something longer (can be overridden by runners)
|
25
|
+
|
26
|
+
def self.debug=(value)
|
27
|
+
@@debug = value
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.timeout=(value)
|
31
|
+
@@timeout = value
|
32
|
+
@@guess_timeout = false # We've set the timeout, don't guess
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.guess_timeout?
|
36
|
+
@@guess_timeout
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(klass_name=nil, method_name=nil, reporter = Reporter.new)
|
40
|
+
super()
|
41
|
+
|
42
|
+
@klass_name = klass_name
|
43
|
+
@method_name = method_name.intern if method_name
|
44
|
+
|
45
|
+
@klass = klass_name.to_class
|
46
|
+
|
47
|
+
@method = nil
|
48
|
+
@reporter = reporter
|
49
|
+
|
50
|
+
self.strict = false
|
51
|
+
self.auto_shift_type = true
|
52
|
+
self.expected = Array
|
53
|
+
|
54
|
+
@mutatees = Hash.new
|
55
|
+
@mutation_count = Hash.new
|
56
|
+
@node_count = Hash.new
|
57
|
+
@count = 0
|
58
|
+
|
59
|
+
MUTATABLE_NODES.each {|type| @mutatees[type] = [] }
|
60
|
+
|
61
|
+
@failures = []
|
62
|
+
|
63
|
+
@mutated = false
|
64
|
+
|
65
|
+
grab_mutatees
|
66
|
+
|
67
|
+
@original_tree = current_tree.deep_clone
|
68
|
+
@original_mutatees = mutatees.deep_clone
|
69
|
+
end
|
70
|
+
|
71
|
+
############################################################
|
72
|
+
### Overwrite test_pass? for your own Heckle runner.
|
73
|
+
def tests_pass?
|
74
|
+
raise NotImplementedError
|
75
|
+
end
|
76
|
+
|
77
|
+
def run_tests
|
78
|
+
if tests_pass? then
|
79
|
+
record_passing_mutation
|
80
|
+
else
|
81
|
+
@reporter.report_test_failures
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
############################################################
|
86
|
+
### Running the script
|
87
|
+
|
88
|
+
def validate
|
89
|
+
if mutations_left == 0
|
90
|
+
@reporter.no_mutations(method_name)
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
@reporter.method_loaded(klass_name, method_name, mutations_left)
|
95
|
+
|
96
|
+
until mutations_left == 0
|
97
|
+
@reporter.remaining_mutations(mutations_left)
|
98
|
+
reset_tree
|
99
|
+
begin
|
100
|
+
process current_tree
|
101
|
+
silence_stream { timeout(@@timeout) { run_tests } }
|
102
|
+
rescue SyntaxError => e
|
103
|
+
@reporter.warning "Mutation caused a syntax error:\n\n#{e.message}}"
|
104
|
+
rescue Timeout::Error
|
105
|
+
@reporter.warning "Your tests timed out. Heckle may have caused an infinite loop."
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
reset # in case we're validating again. we should clean up.
|
110
|
+
|
111
|
+
unless @failures.empty?
|
112
|
+
@reporter.no_failures
|
113
|
+
@failures.each do |failure|
|
114
|
+
@reporter.failure(failure)
|
115
|
+
end
|
116
|
+
else
|
117
|
+
@reporter.no_surviving_mutants
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def record_passing_mutation
|
122
|
+
@failures << current_code
|
123
|
+
end
|
124
|
+
|
125
|
+
def heckle(exp)
|
126
|
+
orig_exp = exp.deep_clone
|
127
|
+
src = begin
|
128
|
+
RubyToRuby.new.process(exp)
|
129
|
+
rescue => e
|
130
|
+
puts "Error: #{e.message} with: #{klass_name}##{method_name}: #{orig_exp.inspect}"
|
131
|
+
raise e
|
132
|
+
end
|
133
|
+
@reporter.replacing(klass_name, method_name, src) if @@debug
|
134
|
+
|
135
|
+
clean_name = method_name.to_s.gsub(/self\./, '')
|
136
|
+
self.count += 1
|
137
|
+
new_name = "h#{count}_#{clean_name}"
|
138
|
+
|
139
|
+
klass = aliasing_class method_name
|
140
|
+
klass.send :remove_method, new_name rescue nil
|
141
|
+
klass.send :alias_method, new_name, clean_name
|
142
|
+
klass.send :remove_method, clean_name rescue nil
|
143
|
+
|
144
|
+
@klass.class_eval src, "(#{new_name})"
|
145
|
+
end
|
146
|
+
|
147
|
+
############################################################
|
148
|
+
### Processing sexps
|
149
|
+
|
150
|
+
def process_defn(exp)
|
151
|
+
self.method = exp.shift
|
152
|
+
result = [:defn, method]
|
153
|
+
result << process(exp.shift) until exp.empty?
|
154
|
+
heckle(result) if method == method_name
|
155
|
+
|
156
|
+
return result
|
157
|
+
ensure
|
158
|
+
@mutated = false
|
159
|
+
reset_node_count
|
160
|
+
end
|
161
|
+
|
162
|
+
def process_lit(exp)
|
163
|
+
mutate_node [:lit, exp.shift]
|
164
|
+
end
|
165
|
+
|
166
|
+
def mutate_lit(exp)
|
167
|
+
case exp[1]
|
168
|
+
when Fixnum, Float, Bignum
|
169
|
+
[:lit, exp[1] + rand_number]
|
170
|
+
when Symbol
|
171
|
+
[:lit, rand_symbol]
|
172
|
+
when Regexp
|
173
|
+
[:lit, /#{Regexp.escape(rand_string)}/]
|
174
|
+
when Range
|
175
|
+
[:lit, rand_range]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def process_str(exp)
|
180
|
+
mutate_node [:str, exp.shift]
|
181
|
+
end
|
182
|
+
|
183
|
+
def mutate_str(node)
|
184
|
+
[:str, rand_string]
|
185
|
+
end
|
186
|
+
|
187
|
+
def process_if(exp)
|
188
|
+
mutate_node [:if, process(exp.shift), process(exp.shift), process(exp.shift)]
|
189
|
+
end
|
190
|
+
|
191
|
+
def mutate_if(node)
|
192
|
+
[:if, node[1], node[3], node[2]]
|
193
|
+
end
|
194
|
+
|
195
|
+
def process_true(exp)
|
196
|
+
mutate_node [:true]
|
197
|
+
end
|
198
|
+
|
199
|
+
def mutate_true(node)
|
200
|
+
[:false]
|
201
|
+
end
|
202
|
+
|
203
|
+
def process_false(exp)
|
204
|
+
mutate_node [:false]
|
205
|
+
end
|
206
|
+
|
207
|
+
def mutate_false(node)
|
208
|
+
[:true]
|
209
|
+
end
|
210
|
+
|
211
|
+
def process_while(exp)
|
212
|
+
cond, body, head_controlled = grab_conditional_loop_parts(exp)
|
213
|
+
mutate_node [:while, cond, body, head_controlled]
|
214
|
+
end
|
215
|
+
|
216
|
+
def mutate_while(node)
|
217
|
+
[:until, node[1], node[2], node[3]]
|
218
|
+
end
|
219
|
+
|
220
|
+
def process_until(exp)
|
221
|
+
cond, body, head_controlled = grab_conditional_loop_parts(exp)
|
222
|
+
mutate_node [:until, cond, body, head_controlled]
|
223
|
+
end
|
224
|
+
|
225
|
+
def mutate_until(node)
|
226
|
+
[:while, node[1], node[2], node[3]]
|
227
|
+
end
|
228
|
+
|
229
|
+
def mutate_node(node)
|
230
|
+
raise UnsupportedNodeError unless respond_to? "mutate_#{node.first}"
|
231
|
+
increment_node_count node
|
232
|
+
|
233
|
+
if should_heckle? node
|
234
|
+
increment_mutation_count node
|
235
|
+
return send("mutate_#{node.first}", node)
|
236
|
+
else
|
237
|
+
node
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
############################################################
|
242
|
+
### Tree operations
|
243
|
+
|
244
|
+
def walk_and_push(node)
|
245
|
+
return unless node.respond_to? :each
|
246
|
+
return if node.is_a? String
|
247
|
+
node.each { |child| walk_and_push(child) }
|
248
|
+
if MUTATABLE_NODES.include? node.first
|
249
|
+
@mutatees[node.first.to_sym].push(node)
|
250
|
+
mutation_count[node] = 0
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def grab_mutatees
|
255
|
+
walk_and_push(current_tree)
|
256
|
+
end
|
257
|
+
|
258
|
+
def current_tree
|
259
|
+
ParseTree.translate(klass_name.to_class, method_name)
|
260
|
+
end
|
261
|
+
|
262
|
+
def reset
|
263
|
+
reset_tree
|
264
|
+
reset_mutatees
|
265
|
+
reset_mutation_count
|
266
|
+
end
|
267
|
+
|
268
|
+
def reset_tree
|
269
|
+
return unless original_tree != current_tree
|
270
|
+
@mutated = false
|
271
|
+
|
272
|
+
self.count += 1
|
273
|
+
|
274
|
+
clean_name = method_name.to_s.gsub(/self\./, '')
|
275
|
+
new_name = "h#{count}_#{clean_name}"
|
276
|
+
|
277
|
+
klass = aliasing_class method_name
|
278
|
+
|
279
|
+
klass.send :undef_method, new_name rescue nil
|
280
|
+
klass.send :alias_method, new_name, clean_name
|
281
|
+
klass.send :alias_method, clean_name, "h1_#{clean_name}"
|
282
|
+
end
|
283
|
+
|
284
|
+
def reset_mutatees
|
285
|
+
@mutatees = @original_mutatees.deep_clone
|
286
|
+
end
|
287
|
+
|
288
|
+
def reset_mutation_count
|
289
|
+
mutation_count.each {|k,v| mutation_count[k] = 0}
|
290
|
+
end
|
291
|
+
|
292
|
+
def reset_node_count
|
293
|
+
node_count.each {|k,v| node_count[k] = 0}
|
294
|
+
end
|
295
|
+
|
296
|
+
def increment_node_count(node)
|
297
|
+
if node_count[node].nil?
|
298
|
+
node_count[node] = 1
|
299
|
+
else
|
300
|
+
node_count[node] += 1
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def increment_mutation_count(node)
|
305
|
+
# So we don't re-mutate this later if the tree is reset
|
306
|
+
mutation_count[node] += 1
|
307
|
+
@mutatees[node.first].delete_at(@mutatees[node.first].index(node))
|
308
|
+
@mutated = true
|
309
|
+
end
|
310
|
+
|
311
|
+
############################################################
|
312
|
+
### Convenience methods
|
313
|
+
|
314
|
+
def aliasing_class(method_name)
|
315
|
+
method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
|
316
|
+
end
|
317
|
+
|
318
|
+
def should_heckle?(exp)
|
319
|
+
return false unless method == method_name
|
320
|
+
mutation_count[exp] = 0 if mutation_count[exp].nil?
|
321
|
+
return false if node_count[exp] <= mutation_count[exp]
|
322
|
+
( mutatees[exp.first.to_sym] || [] ).include?(exp) && !already_mutated?
|
323
|
+
end
|
324
|
+
|
325
|
+
def grab_conditional_loop_parts(exp)
|
326
|
+
cond = process(exp.shift)
|
327
|
+
body = process(exp.shift)
|
328
|
+
head_controlled = exp.shift
|
329
|
+
return cond, body, head_controlled
|
330
|
+
end
|
331
|
+
|
332
|
+
def already_mutated?
|
333
|
+
@mutated
|
334
|
+
end
|
335
|
+
|
336
|
+
def mutations_left
|
337
|
+
sum = 0
|
338
|
+
@mutatees.each {|mut| sum += mut.last.size }
|
339
|
+
sum
|
340
|
+
end
|
341
|
+
|
342
|
+
def current_code
|
343
|
+
RubyToRuby.translate(klass_name.to_class, method_name)
|
344
|
+
end
|
345
|
+
|
346
|
+
def rand_number
|
347
|
+
(rand(10) + 1)*((-1)**rand(2))
|
348
|
+
end
|
349
|
+
|
350
|
+
def rand_string
|
351
|
+
size = rand(50)
|
352
|
+
str = ""
|
353
|
+
size.times { str << rand(126).chr }
|
354
|
+
str
|
355
|
+
end
|
356
|
+
|
357
|
+
def rand_symbol
|
358
|
+
letters = ('a'..'z').to_a + ('A'..'Z').to_a
|
359
|
+
str = ""
|
360
|
+
(rand(50) + 1).times { str << letters[rand(letters.size)] }
|
361
|
+
:"#{str}"
|
362
|
+
end
|
363
|
+
|
364
|
+
def rand_range
|
365
|
+
min = rand(50)
|
366
|
+
max = min + rand(50)
|
367
|
+
min..max
|
368
|
+
end
|
369
|
+
|
370
|
+
def silence_stream
|
371
|
+
dead = File.open("/dev/null", "w")
|
372
|
+
|
373
|
+
$stdout.flush
|
374
|
+
$stderr.flush
|
375
|
+
|
376
|
+
oldstdout = $stdout.dup
|
377
|
+
oldstderr = $stderr.dup
|
378
|
+
|
379
|
+
$stdout.reopen(dead)
|
380
|
+
$stderr.reopen(dead)
|
381
|
+
|
382
|
+
result = yield
|
383
|
+
|
384
|
+
ensure
|
385
|
+
$stdout.flush
|
386
|
+
$stderr.flush
|
387
|
+
|
388
|
+
$stdout.reopen(oldstdout)
|
389
|
+
$stderr.reopen(oldstderr)
|
390
|
+
result
|
391
|
+
end
|
392
|
+
|
393
|
+
class Reporter
|
394
|
+
def no_mutations(method_name)
|
395
|
+
warning "#{method_name} has a thick skin. There's nothing to heckle."
|
396
|
+
end
|
397
|
+
|
398
|
+
def method_loaded(klass_name, method_name, mutations_left)
|
399
|
+
info "#{klass_name}\##{method_name} loaded with #{mutations_left} possible mutations"
|
400
|
+
end
|
401
|
+
|
402
|
+
def remaining_mutations(mutations_left)
|
403
|
+
puts "#{mutations_left} mutations remaining..."
|
404
|
+
end
|
405
|
+
|
406
|
+
def warning(message)
|
407
|
+
puts
|
408
|
+
puts "!" * 70
|
409
|
+
puts "!!! #{message}"
|
410
|
+
puts "!" * 70
|
411
|
+
puts
|
412
|
+
end
|
413
|
+
|
414
|
+
def info(message)
|
415
|
+
puts
|
416
|
+
puts "*"*70
|
417
|
+
puts "*** #{message}"
|
418
|
+
puts "*"*70
|
419
|
+
puts
|
420
|
+
end
|
421
|
+
|
422
|
+
def no_failures
|
423
|
+
puts "\nThe following mutations didn't cause test failures:\n"
|
424
|
+
end
|
425
|
+
|
426
|
+
def failure(failure)
|
427
|
+
puts "\n#{failure}\n"
|
428
|
+
end
|
429
|
+
|
430
|
+
def no_surviving_mutants
|
431
|
+
puts "No mutants survived. Cool!\n\n"
|
432
|
+
end
|
433
|
+
|
434
|
+
def replacing(klass_name, method_name, src)
|
435
|
+
puts "Replacing #{klass_name}##{method_name} with:\n\n#{src}\n"
|
436
|
+
end
|
437
|
+
|
438
|
+
def report_test_failures
|
439
|
+
puts "Tests failed -- this is good"
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
data/lib/test_unit_heckler.rb
CHANGED
@@ -4,14 +4,14 @@ require 'test/unit/autorunner'
|
|
4
4
|
require 'heckle'
|
5
5
|
$: << 'lib' << 'test'
|
6
6
|
|
7
|
-
class TestUnitHeckler < Heckle
|
7
|
+
class TestUnitHeckler < Heckle
|
8
8
|
@@test_pattern = 'test/test_*.rb'
|
9
9
|
@@tests_loaded = false;
|
10
10
|
|
11
11
|
def self.test_pattern=(value)
|
12
12
|
@@test_pattern = value
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def self.load_test_files
|
16
16
|
@@tests_loaded = true
|
17
17
|
Dir.glob(@@test_pattern).each {|test| require test}
|
@@ -21,13 +21,30 @@ class TestUnitHeckler < Heckle::Base
|
|
21
21
|
load_test_files
|
22
22
|
klass = klass_name.to_class
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
initial_time = Time.now
|
25
|
+
|
26
|
+
unless self.new(klass_name).tests_pass? then
|
27
|
+
abort "Initial run of tests failed... fix and run heckle again"
|
28
|
+
end
|
29
|
+
|
30
|
+
if self.guess_timeout?
|
31
|
+
running_time = (Time.now - initial_time)
|
32
|
+
adjusted_timeout = (running_time * 2 < 5) ? 5 : (running_time * 2)
|
33
|
+
self.timeout = adjusted_timeout
|
34
|
+
puts "Setting timeout at #{adjusted_timeout} seconds." if @@debug
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "Initial tests pass. Let's rumble."
|
39
|
+
self.timeout = adjusted_timeout
|
40
|
+
|
41
|
+
puts "Initial tests pass. Let's rumble."
|
42
|
+
|
43
|
+
klass_methods = klass.singleton_methods(false).collect {|meth| "self.#{meth}"}
|
44
|
+
methods = method_name ? Array(method_name) : klass.instance_methods(false) + klass_methods
|
45
|
+
|
46
|
+
methods.each do |method_name|
|
47
|
+
self.new(klass_name, method_name).validate
|
31
48
|
end
|
32
49
|
end
|
33
50
|
|
@@ -35,17 +52,10 @@ class TestUnitHeckler < Heckle::Base
|
|
35
52
|
super(klass_name, method_name)
|
36
53
|
self.class.load_test_files unless @@tests_loaded
|
37
54
|
end
|
38
|
-
|
39
|
-
def test_and_validate
|
40
|
-
if silence_stream(STDOUT) { tests_pass? } then
|
41
|
-
puts "Initial tests pass. Let's rumble."
|
42
|
-
validate
|
43
|
-
else
|
44
|
-
puts "Tests failed... fix and run heckle again"
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
55
|
+
|
48
56
|
def tests_pass?
|
49
|
-
|
57
|
+
silence_stream do
|
58
|
+
Test::Unit::AutoRunner.run
|
59
|
+
end
|
50
60
|
end
|
51
61
|
end
|
data/sample/lib/heckled.rb
CHANGED
@@ -5,6 +5,10 @@ class Heckled
|
|
5
5
|
@names = []
|
6
6
|
end
|
7
7
|
|
8
|
+
def self.is_a_klass_method?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
8
12
|
def uses_while
|
9
13
|
i = 1
|
10
14
|
while i < 10
|
@@ -64,4 +68,13 @@ class Heckled
|
|
64
68
|
def uses_masignment
|
65
69
|
one, two = [1, 2]
|
66
70
|
end
|
71
|
+
|
72
|
+
def uses_infinite_loop?
|
73
|
+
# Converts to a infinite loop actually
|
74
|
+
some_func until true
|
75
|
+
end
|
76
|
+
|
77
|
+
# placeholder
|
78
|
+
def some_func
|
79
|
+
end
|
67
80
|
end
|
data/sample/test/test_heckled.rb
CHANGED
@@ -16,4 +16,12 @@ class TestHeckled < Test::Unit::TestCase
|
|
16
16
|
@heckled.uses_strings
|
17
17
|
assert_equal ["Hello, Robert", "Hello, Jeff", "Hi, Frank"], @heckled.names
|
18
18
|
end
|
19
|
+
|
20
|
+
def test_uses_infinite_loop
|
21
|
+
@heckled.uses_infinite_loop?
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_is_a_klass_method
|
25
|
+
assert_equal true, Heckled.is_a_klass_method?
|
26
|
+
end
|
19
27
|
end
|