butterfly_net 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +94 -39
- data/lib/butterfly_net/commands.rb +12 -26
- data/lib/butterfly_net/file_writer.rb +9 -59
- data/lib/butterfly_net/rails_test_unit_adapter.rb +9 -0
- data/lib/butterfly_net/readline_reader.rb +74 -0
- data/lib/butterfly_net/test_case.rb +43 -0
- data/lib/butterfly_net/test_unit_adapter.rb +6 -44
- data/lib/butterfly_net/test_unit_method.rb +55 -11
- data/lib/butterfly_net.rb +3 -0
- data/test/butterfly_net_tests.rb +1 -1
- data/test/{test_unit_adapter_test.rb → test_case_test.rb} +52 -10
- data/test/test_unit_method_test.rb +64 -18
- metadata +6 -3
data/README.md
CHANGED
@@ -1,21 +1,35 @@
|
|
1
1
|
Butterfly Net
|
2
2
|
=============
|
3
3
|
|
4
|
-
[Home page and source]: http://butterflynet.org
|
5
|
-
[Rubygems page]: http://rubygems.org/gems/butterfly_net
|
6
4
|
|
7
5
|
Author: Chris Smith (quartzmo -at- gmail.com)
|
8
6
|
|
7
|
+
[Home page and source download](http://github.com/quartzmo/butterfly_net)
|
9
8
|
|
10
|
-
|
9
|
+
[RubyGems page](http://rubygems.org/gems/butterfly_net)
|
10
|
+
|
11
|
+
|
12
|
+
## Summary
|
11
13
|
|
12
14
|
IRB and Rails console history captured as Test::Unit tests. (RSpec and others hopefully soon to come.)
|
13
15
|
|
16
|
+
## Description
|
17
|
+
|
18
|
+
Butterfly Net is intended to help you capture, as executable tests, surprising or unexpected behavior that you come across while
|
19
|
+
interacting with your project code in IRB.
|
20
|
+
|
21
|
+
** Warning **
|
22
|
+
|
23
|
+
Butterfly Net is not a tool for [test-first](http://www.extremeprogramming.org/rules/testfirst.html) development, and is
|
24
|
+
not intended as a primary tool for [Test-Driven Development](http://en.wikipedia.org/wiki/Test-driven_development) (TDD).
|
25
|
+
Please use it in addition to these valuable methods.
|
14
26
|
|
15
|
-
##
|
27
|
+
## Install
|
16
28
|
|
17
|
-
Butterfly Net is available as a gem from rubygems.org
|
18
|
-
|
29
|
+
Butterfly Net is available as a gem from [rubygems.org](http://rubygems.org/gems/butterfly_net), or as source from
|
30
|
+
[GitHub](http://github.com/quartzmo/butterfly_net).
|
31
|
+
|
32
|
+
To install the gem:
|
19
33
|
|
20
34
|
sudo gem install butterfly_net
|
21
35
|
|
@@ -25,36 +39,40 @@ To automatically require Butterfly Net on every IRB session, add the following t
|
|
25
39
|
require 'butterfly_net'
|
26
40
|
|
27
41
|
|
28
|
-
##
|
42
|
+
## Usage
|
29
43
|
|
30
44
|
### Command methods
|
31
45
|
|
32
|
-
The following commands can be used in any IRB-based console.
|
46
|
+
The following commands can be used in any IRB-based console. The shortcuts should be preferred, the longer aliases exist in case of naming conflicts.
|
33
47
|
|
34
|
-
* bn
|
35
|
-
* bnc
|
36
|
-
* m
|
48
|
+
* `bn(file_name=nil)`, `bn_open(file_name=nil)` Open a new test case, closing the current test case if one exists. Args: file_name:string (optional; '.rb' will appended if needed)
|
49
|
+
* `bnc`, `bn_close` Close the active test case, and write the output to a file.
|
50
|
+
* `m(method_name=nil)`, `bn_method(method_name=nil)` Close the current test method (or block), naming it with the arg method_name:string (optional)
|
37
51
|
|
38
52
|
### Example Usage in IRB
|
39
53
|
|
40
54
|
$ irb
|
41
|
-
irb(main):001:0>
|
55
|
+
irb(main):001:0> require 'rubygems'
|
56
|
+
=> true
|
57
|
+
irb(main):002:0> require 'butterfly_net'
|
42
58
|
=> true
|
43
|
-
irb(main):
|
59
|
+
irb(main):003:0> bn 'irb_tests'
|
60
|
+
=> true
|
61
|
+
irb(main):004:0> a = 1
|
44
62
|
=> 1
|
45
|
-
irb(main):
|
63
|
+
irb(main):005:0> a += 2
|
46
64
|
=> 3
|
47
|
-
irb(main):
|
65
|
+
irb(main):006:0> m 'plusequals'
|
48
66
|
=> true
|
49
|
-
irb(main):
|
67
|
+
irb(main):007:0> require 'bigdecimal'
|
50
68
|
=> true
|
51
|
-
irb(main):
|
69
|
+
irb(main):008:0> infinity = BigDecimal('Infinity')
|
52
70
|
=> #<BigDecimal:114ed34,'Infinity',4(4)>
|
53
|
-
irb(main):
|
71
|
+
irb(main):009:0> BigDecimal.new("1.0") / BigDecimal.new("0.0") == infinity
|
54
72
|
=> true
|
55
|
-
irb(main):
|
73
|
+
irb(main):010:0> m 'bigdecimal_infinity'
|
56
74
|
=> true
|
57
|
-
irb(main):
|
75
|
+
irb(main):011:0> exit
|
58
76
|
butterfly_net: irb_tests.rb closed
|
59
77
|
true
|
60
78
|
$ cat irb_tests.rb
|
@@ -86,11 +104,13 @@ The following commands can be used in any IRB-based console.
|
|
86
104
|
|
87
105
|
### Ruby on Rails console
|
88
106
|
|
89
|
-
For repeatable tests, be sure to load the Rails test environment with
|
90
|
-
In a Rails project, you can run
|
91
|
-
|
107
|
+
For repeatable tests, be sure to load the Rails test environment with `./script/console test`. Invoke the `bn` command
|
108
|
+
with the relative path to the appropriate test sub-directory, and file name. (Hint: In a Rails project, you can run an
|
109
|
+
individual test by adding the test directory to the path with the option -I when you invoke Ruby.)
|
110
|
+
|
111
|
+
For example:
|
92
112
|
|
93
|
-
|
113
|
+
$ ./script/console test
|
94
114
|
Loading test environment (Rails 2.3.4)
|
95
115
|
>> bn "test/unit/person_console_test"
|
96
116
|
. . .
|
@@ -98,33 +118,41 @@ or an individual test by adding the test directory to the path with the option -
|
|
98
118
|
=> 2
|
99
119
|
>> exit
|
100
120
|
. . .
|
101
|
-
|
121
|
+
$ ruby -Itest test/unit/person_console_test.rb
|
102
122
|
. . .
|
103
123
|
1 tests, 1 assertions, 0 failures, 0 errors
|
104
124
|
|
105
125
|
|
106
|
-
##
|
126
|
+
## Known Issues
|
107
127
|
|
108
128
|
This section covers issues which may not be resolved quickly. Feel free to lend a hand!
|
109
129
|
|
110
|
-
### Object
|
130
|
+
### The return value of Object#inspect is often not valid Ruby
|
111
131
|
|
112
|
-
|
113
|
-
|
114
|
-
|
132
|
+
Currently, the expectation that Butterfly Net places into an assertion is the output of Object#inspect. Simple types and value-oriented
|
133
|
+
types such as Hash and Array usually do return valid code, which works great. Unfortunately, most other types respond with the familiar
|
134
|
+
`#<...>` notation, which can't be interpreted.
|
115
135
|
|
116
136
|
For example:
|
117
137
|
|
118
138
|
assert_equal(#<BigDecimal:11511d8,'Infinity',4(24)>, BigDecimal.new("1.0") / BigDecimal.new("0.0")) # doesn't work
|
119
139
|
|
120
|
-
The workaround is to
|
121
|
-
|
140
|
+
The best workaround is to use IRB in a way that gets you to simple types, the same way you write unit tests that
|
141
|
+
compare values by calling `to_s`, `to_i`, `to_f`, etc on more complex objects.
|
142
|
+
Another solution, more appropriate for cases like the BigDecimal infinity example above, is to assign the expected
|
143
|
+
result to a variable.
|
144
|
+
|
145
|
+
infinity = BigDecimal('Infinity')
|
146
|
+
assert(BigDecimal.new("1.0") / BigDecimal.new("0.0") == infinity) # works great, IF you know what to expect
|
147
|
+
|
148
|
+
Of course, you have to know what to expect in order to do this, which may take a few tries. Sorry.
|
149
|
+
I'll be searching for a better solution to this one.
|
122
150
|
|
123
151
|
|
124
|
-
###
|
152
|
+
### Assigning a variable, even within a string, results in no assertion for that line
|
125
153
|
|
126
|
-
Butterfly Net
|
127
|
-
|
154
|
+
To keep tests readable, Butterfly Net writes simple assignment expressions such as "a = 1" without enclosing assertions.
|
155
|
+
However, the regular expression it uses to accomplish this can cause Butterfly Net to miss some tests.
|
128
156
|
|
129
157
|
For example:
|
130
158
|
|
@@ -136,14 +164,41 @@ For example:
|
|
136
164
|
results in
|
137
165
|
|
138
166
|
def test_1
|
139
|
-
a = 1; a + 1
|
140
|
-
"a=1".split('=')
|
167
|
+
a = 1; a + 1 # should have been a = 1; assert_equal(2, a + 1)
|
168
|
+
"a=1".split('=') # should have been assert_equal(["a", "1"],"a=1".split('='))
|
141
169
|
end
|
142
170
|
|
143
|
-
|
171
|
+
Maybe someone can suggest how Butterfly Net can become just enough of an actual Ruby interpreter to get past this issue?
|
172
|
+
In the meantime, just put assignments (and anything that looks like an assignment) on separate lines
|
173
|
+
from the expressions you want to be tested.
|
174
|
+
|
175
|
+
|
176
|
+
## Story
|
177
|
+
|
178
|
+
Coming to Ruby from Java, I felt "The Console" was like a magical English butler, always there discreetly
|
179
|
+
at my side to instantly and precisely answer all my questions. How incredible!
|
180
|
+
|
181
|
+
In Java, JUnit had been my preferred tool for exploring code, both my own project code and
|
182
|
+
third-party libraries and frameworks. As anyone who has worked on a legacy (i.e., test-deprived) codebase or experienced
|
183
|
+
java.util.Calendar knows, the tighter the feedback loop, the sooner you get your answers. Experimenting with code behavior through an
|
184
|
+
application UI is only for the foolish, or those racking up billable hours on a big waterfall project.
|
185
|
+
JUnit tests were usually the fastest way I could experience what code did in real life, and with IntelliJ IDEA generating the
|
186
|
+
boilerplate at a keystroke, exploratory testing was quick and very acceptable. When I was done learning and
|
187
|
+
experimenting, I deleted most of the tests, in accordance with the idea that less is better, and you shouldn't
|
188
|
+
run automated regression tests for stable, third-party code.
|
189
|
+
|
190
|
+
Switching to Ruby, I found IRB to be a much better tool, but as with all great conveniences, it had a downside.
|
191
|
+
Although I generally program in a test-first style, I also explore existing code after it is written,
|
192
|
+
kicking the tires and banging on the doors. With JUnit, any interesting behavior I discovered during this process was captured in a
|
193
|
+
test, and was duly added to the suite. With IRB? Gone, flown off into the deep blue yonder. The idea for Butterfly Net
|
194
|
+
occurred to me pretty quickly. That's the story, so far. I'm not sure how others will use Butterfly Net, but I don't
|
195
|
+
want to limit the possibilities. Should it become more generalized and flexible, something of a view framework like
|
196
|
+
[Hirb](http://github.com/cldwalker/hirb)? Or did I re-invent something that already existed?
|
197
|
+
|
198
|
+
Please send comments and suggestions to quartzmo -at- gmail.com
|
144
199
|
|
145
200
|
|
146
|
-
##
|
201
|
+
## License
|
147
202
|
|
148
203
|
The MIT License
|
149
204
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# bn - open a new Butterfly Net test case, with optional name
|
5
5
|
# bnc - close Butterfly Net test case, writing output file
|
6
|
+
# m - close current test method, and open a new one
|
6
7
|
# bns - add preceding line to setup method, instead of current assertion set # todo
|
7
8
|
# bnt - add preceding line to teardown method, instead of current assertion set # todo
|
8
9
|
#
|
@@ -10,27 +11,20 @@ module ButterflyNet
|
|
10
11
|
module Commands
|
11
12
|
|
12
13
|
def bn_open(file_name=nil)
|
13
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
check_for_readline
|
17
|
-
# todo: if Rails, WARNING if env is not test
|
18
|
-
|
19
|
-
start_index = Readline::HISTORY.empty? ? 0 : (Readline::HISTORY.size - 1)
|
20
|
-
file_name ||= "butterfly_net_#{Time.now.strftime("%Y%m%d%H%M%S")}.rb"
|
21
|
-
file_name += ".rb" unless file_name =~ /.rb$/
|
22
|
-
@file = FileWriter.new(file_name, start_index)
|
23
|
-
Kernel.at_exit { puts @file.close if @file; @file = nil }
|
14
|
+
@file_writer.close if @file_writer
|
15
|
+
@file_writer = ReadlineReader.new(file_name)
|
16
|
+
Kernel.at_exit { puts @file_writer.close if @file_writer; @file_writer = nil }
|
24
17
|
true
|
25
|
-
rescue
|
18
|
+
rescue Exception => e
|
19
|
+
puts e
|
20
|
+
puts e.backtrace
|
26
21
|
false
|
27
22
|
end
|
28
23
|
|
29
24
|
def bn_close
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@file = nil
|
25
|
+
if @file_writer
|
26
|
+
status = @file_writer.close
|
27
|
+
@file_writer = nil
|
34
28
|
status
|
35
29
|
else
|
36
30
|
puts "butterfly_net: First invoke 'bn' or 'bn_open' to begin a session"
|
@@ -39,25 +33,17 @@ module ButterflyNet
|
|
39
33
|
end
|
40
34
|
|
41
35
|
def bn_method(method_name=nil)
|
42
|
-
|
43
|
-
|
44
|
-
@file.new_assertion_set(method_name)
|
36
|
+
if @file_writer
|
37
|
+
@file_writer.new_assertion_set(method_name)
|
45
38
|
else
|
46
39
|
puts "butterfly_net: First invoke 'bn' or 'bn_open' to begin a session"
|
47
40
|
false
|
48
41
|
end
|
49
42
|
end
|
50
43
|
|
51
|
-
|
52
44
|
alias :bn :bn_open
|
53
45
|
alias :bnc :bn_close
|
54
46
|
alias :m :bn_method
|
55
47
|
|
56
|
-
private
|
57
|
-
|
58
|
-
def check_for_readline
|
59
|
-
raise "butterfly_net: Readline::HISTORY required!" unless defined? Readline::HISTORY
|
60
|
-
end
|
61
|
-
|
62
48
|
end
|
63
49
|
end
|
@@ -1,69 +1,19 @@
|
|
1
1
|
module ButterflyNet
|
2
2
|
class FileWriter
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(file_name, start_index)
|
8
|
-
@file_name = file_name
|
9
|
-
@start_index = start_index
|
10
|
-
@adapter = TestUnitAdapter.new
|
4
|
+
def initialize(filename)
|
5
|
+
@file = File.open(filename, 'a+')
|
11
6
|
end
|
12
7
|
|
13
|
-
def new_assertion_set(method_name)
|
14
|
-
lines = Readline::HISTORY.to_a
|
15
|
-
@end_index ||= Readline::HISTORY.size - 3 # in case of no call to 'bn_close'
|
16
|
-
if @end_index < 0 || lines[@start_index..@end_index].empty?
|
17
|
-
false
|
18
|
-
else
|
19
|
-
|
20
|
-
|
21
|
-
lines[@start_index..@end_index].each do |line|
|
22
|
-
@adapter.add_command(line)
|
23
|
-
end
|
24
8
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
9
|
+
def create_file(data)
|
10
|
+
@file.puts data
|
11
|
+
true
|
12
|
+
ensure
|
13
|
+
if @file # todo: closing is always good, but prevent writing partial data in case of exception
|
14
|
+
@file.close
|
30
15
|
end
|
31
|
-
rescue Exception => e
|
32
|
-
puts "butterfly_net: Error generating tests: #{e}"
|
33
|
-
puts e.backtrace
|
34
|
-
false
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
def close
|
39
|
-
lines = Readline::HISTORY.to_a
|
40
|
-
@end_index ||= Readline::HISTORY.size - 3 # in case of no call to 'bn_close'
|
41
|
-
# if @adapter.empty?
|
42
|
-
# puts "butterfly_net: #{@file_name} closed, no file written to disk."
|
43
|
-
#
|
44
|
-
# false
|
45
|
-
#
|
46
|
-
# else
|
47
|
-
if @end_index < 0 || lines[@start_index..@end_index].empty?
|
48
|
-
puts "butterfly_net: #{@file_name} closed"
|
49
|
-
else
|
50
|
-
lines[@start_index..@end_index].each do |line|
|
51
|
-
@adapter.add_command(line)
|
52
|
-
end
|
53
|
-
puts "butterfly_net: #{@file_name} closed, with #{@end_index - @start_index + 1} lines, from '#{lines[@start_index]}' to '#{lines[@end_index]}'"
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
@adapter.create_file(@file_name)
|
58
|
-
|
59
|
-
true
|
60
|
-
# end
|
61
|
-
rescue Exception => e
|
62
|
-
puts "butterfly_net: Error generating tests: #{e}"
|
63
|
-
puts e.backtrace
|
64
|
-
false
|
65
16
|
end
|
66
17
|
|
67
18
|
end
|
68
|
-
|
69
|
-
end
|
19
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ButterflyNet
|
2
|
+
class ReadlineReader
|
3
|
+
|
4
|
+
attr_accessor :end_index
|
5
|
+
attr_reader :start_index
|
6
|
+
|
7
|
+
def initialize(file_name)
|
8
|
+
|
9
|
+
check_for_readline
|
10
|
+
# todo: if Rails, WARNING if env is not test
|
11
|
+
|
12
|
+
@start_index = Readline::HISTORY.empty? ? 0 : (Readline::HISTORY.size - 1)
|
13
|
+
if file_name
|
14
|
+
@file_name = file_name.to_s + ".rb" unless file_name.to_s =~ /.rb$/
|
15
|
+
else
|
16
|
+
@file_name = "butterfly_net_#{Time.now.strftime("%Y%m%d%H%M%S")}.rb" # todo: something like heroku's generated names
|
17
|
+
end
|
18
|
+
@test_case = TestCase.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def new_assertion_set(method_name)
|
22
|
+
check_for_readline
|
23
|
+
|
24
|
+
lines = Readline::HISTORY.to_a
|
25
|
+
@end_index ||= Readline::HISTORY.size - 3 # in case of no call to 'bn_close'
|
26
|
+
if @end_index < 0 || lines[@start_index..@end_index].empty?
|
27
|
+
false
|
28
|
+
else
|
29
|
+
lines[@start_index..@end_index].each do |line|
|
30
|
+
@test_case.add_command(line)
|
31
|
+
end
|
32
|
+
|
33
|
+
@test_case.close_assertion_set(method_name)
|
34
|
+
@start_index = Readline::HISTORY.size - 1
|
35
|
+
@end_index = nil
|
36
|
+
|
37
|
+
true
|
38
|
+
end
|
39
|
+
rescue Exception => e
|
40
|
+
puts "butterfly_net: Error generating tests: #{e}"
|
41
|
+
puts e.backtrace
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def close
|
47
|
+
check_for_readline
|
48
|
+
lines = Readline::HISTORY.to_a
|
49
|
+
@end_index ||= Readline::HISTORY.size - 3 # in case of no call to 'bn_close'
|
50
|
+
if @end_index < 0 || lines[@start_index..@end_index].empty?
|
51
|
+
else
|
52
|
+
lines[@start_index..@end_index].each do |line|
|
53
|
+
@test_case.add_command(line)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
result = @test_case.create_file(@file_name)
|
59
|
+
puts result ? "butterfly_net: #{@file_name} created." : "butterfly_net: #{@file_name} was not created. No tests were generated from this session."
|
60
|
+
result
|
61
|
+
|
62
|
+
rescue Exception => e
|
63
|
+
puts "butterfly_net: Error generating tests: #{e}"
|
64
|
+
puts e.backtrace
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def check_for_readline
|
70
|
+
raise "butterfly_net: Readline::HISTORY required!" unless defined? Readline::HISTORY
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ButterflyNet
|
2
|
+
class TestCase
|
3
|
+
|
4
|
+
attr_reader :assertion_sets
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
rails = defined? ActiveSupport::TestCase # todo support earlier versions of rails?
|
8
|
+
@adapter = rails ? RailsTestUnitAdapter.new : TestUnitAdapter.new
|
9
|
+
@assertion_sets = [@adapter.test_method]
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_command(line)
|
13
|
+
@assertion_sets.last << line
|
14
|
+
end
|
15
|
+
|
16
|
+
# user provides name at closing
|
17
|
+
def close_assertion_set(method_name = nil)
|
18
|
+
@assertion_sets.last.name = (method_name ? method_name : @assertion_sets.size)
|
19
|
+
@assertion_sets << @adapter.test_method
|
20
|
+
end
|
21
|
+
|
22
|
+
# true if ALL are empty
|
23
|
+
def empty?
|
24
|
+
@assertion_sets.inject(true) {|result,e| result = (result and e.empty?); result}
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_file(filename)
|
28
|
+
bodytext = generate_bodytext
|
29
|
+
return false if empty? || bodytext.empty?
|
30
|
+
FileWriter.new(filename).create_file(@adapter.header_text + bodytext + @adapter.footer_text)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_methods
|
34
|
+
@assertion_sets.last.name = @assertion_sets.size unless @assertion_sets.last.name # assign the default, numbered name; done here for testing
|
35
|
+
@assertion_sets.collect {|i| i.text }.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_bodytext
|
39
|
+
test_methods.inject("") { |result,e|result += e; result }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -1,54 +1,16 @@
|
|
1
1
|
module ButterflyNet
|
2
2
|
class TestUnitAdapter
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
def initialize
|
7
|
-
@assertion_sets = []
|
8
|
-
end
|
9
|
-
|
10
|
-
def add_command(line)
|
11
|
-
@assertion_sets << TestUnitMethod.new unless @assertion_sets.last
|
12
|
-
@assertion_sets.last << line
|
13
|
-
end
|
14
|
-
|
15
|
-
def close_assertion_set(method_name = nil)
|
16
|
-
@assertion_sets.last.name = (method_name ? method_name : @assertion_sets.size)
|
17
|
-
@assertion_sets << TestUnitMethod.new
|
18
|
-
end
|
19
|
-
|
20
|
-
def empty?
|
21
|
-
@assertion_sets.empty?
|
4
|
+
def header_text
|
5
|
+
"require \"test/unit\"\n\n# IRB test capture courtesy of butterfly_net (butterflynet.org)\nclass MyTest < Test::Unit::TestCase\n\n"
|
22
6
|
end
|
23
7
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
#todo detect existing file, and delete last 'end' line
|
28
|
-
file = File.open(filename, 'a+') # starts at end of file if file exists
|
29
|
-
|
30
|
-
if defined? ActiveSupport::TestCase # rails # todo support earlier versions of rails
|
31
|
-
file.puts "require \"test_helper\"\n\n# script/console test capture courtesy of butterfly_net (butterflynet.org)\nclass MyTest < ActiveSupport::TestCase"
|
32
|
-
else
|
33
|
-
file.puts "require \"test/unit\"\n\n# IRB test capture courtesy of butterfly_net (butterflynet.org)\nclass MyTest < Test::Unit::TestCase"
|
34
|
-
end
|
35
|
-
|
36
|
-
|
37
|
-
test_methods.each do |test_method|
|
38
|
-
file.puts "\n #{test_method}"
|
39
|
-
end
|
40
|
-
|
41
|
-
ensure
|
42
|
-
if file # todo: closing is always good, but prevent writing partial data in case of exception
|
43
|
-
file.puts "\nend"
|
44
|
-
file.close
|
45
|
-
end
|
46
|
-
|
8
|
+
def footer_text
|
9
|
+
'end'
|
47
10
|
end
|
48
11
|
|
49
|
-
def
|
50
|
-
|
51
|
-
@assertion_sets.collect {|i| i.text }.compact
|
12
|
+
def test_method
|
13
|
+
TestUnitMethod.new
|
52
14
|
end
|
53
15
|
|
54
16
|
end
|
@@ -5,29 +5,57 @@ module ButterflyNet
|
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
@commands = []
|
8
|
+
@definitions = []
|
8
9
|
end
|
9
10
|
|
10
11
|
def <<(line)
|
11
12
|
@commands << line
|
12
13
|
end
|
13
14
|
|
15
|
+
def empty?
|
16
|
+
@commands.empty?
|
17
|
+
end
|
18
|
+
|
14
19
|
def name=(name)
|
15
20
|
name = underscore(name)
|
16
|
-
@name = name =~
|
21
|
+
@name = name =~ /^\s*test_/ ? name : "test_#{name}"
|
17
22
|
end
|
18
23
|
|
19
24
|
def self.assignment_or_require?(line) # todo: extract to line class
|
20
25
|
line =~ /require\s*['|"]\w+['|"]|[^=<>!*%\/+-\\|&]=[^=~]/
|
21
26
|
end
|
22
27
|
|
28
|
+
def self.start_def?(line) # todo: extract to line class
|
29
|
+
line =~ /^\s*def\s+|^\s*class\s+|^\s*module\s+/
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.end_def?(line) # todo: extract to line class
|
33
|
+
line =~ /end\s*$/
|
34
|
+
end
|
35
|
+
|
23
36
|
def text
|
24
37
|
purge_bad_commands
|
38
|
+
before_test_method = ""
|
25
39
|
lines_string = ""
|
26
|
-
|
27
|
-
|
28
|
-
|
40
|
+
nesting_level = 0
|
41
|
+
@commands.each_with_index do |current_line, i|
|
42
|
+
if TestUnitMethod.start_def?(current_line) and TestUnitMethod.end_def?(current_line)
|
43
|
+
before_test_method += " #{(" " * nesting_level)}#{current_line}\n\n"
|
44
|
+
elsif TestUnitMethod.start_def?(current_line)
|
45
|
+
nesting_level += 1
|
46
|
+
before_test_method += "#{(" " * nesting_level)}#{current_line}\n"
|
47
|
+
elsif TestUnitMethod.end_def?(current_line)
|
48
|
+
nesting_level -= 1
|
49
|
+
before_test_method += " #{(" " * nesting_level)}#{current_line}\n#{ nesting_level == 0 ? "\n" : ""}"
|
50
|
+
elsif nesting_level == 0
|
51
|
+
text = assertion(i)
|
52
|
+
lines_string += " #{text}\n" if text
|
53
|
+
else
|
54
|
+
before_test_method += " #{(" " * nesting_level)}#{current_line}\n"
|
55
|
+
end
|
29
56
|
end
|
30
|
-
|
57
|
+
before_test_method += " "
|
58
|
+
lines_string.empty? ? nil : "#{before_test_method}def #{@name}\n#{lines_string} end\n\n"
|
31
59
|
end
|
32
60
|
|
33
61
|
def assertion(current_i)
|
@@ -63,33 +91,49 @@ module ButterflyNet
|
|
63
91
|
else
|
64
92
|
|
65
93
|
# any other sort of object is handled as a not equal assertion
|
66
|
-
"assert_not_equal((#{current_line}), #{current_line})"
|
94
|
+
"assert_not_equal((#{current_line}), #{current_line})" # todo assert_not_nil in some cases?
|
67
95
|
end
|
68
96
|
|
69
97
|
end
|
70
98
|
|
71
99
|
def instances_equal_by_value?(instance)
|
72
|
-
instance == instance.dup rescue true # can't dup
|
100
|
+
instance == instance.dup rescue true # assume anything like Fixnum that can't be dup'd is a value type...
|
73
101
|
end
|
74
102
|
|
75
103
|
private
|
76
104
|
|
105
|
+
# def extract_definitions # work in progress, todo: handle definitions separately, place at top of file
|
106
|
+
# @commands.each_with_index do |current_line, i|
|
107
|
+
# if TestUnitMethod.start_def?(current_line) or TestUnitMethod.end_def?(current_line)
|
108
|
+
# @definitions << current_line
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
# end
|
77
112
|
|
78
113
|
def purge_bad_commands
|
79
114
|
begin
|
80
115
|
commands = ""
|
81
116
|
index = 0
|
117
|
+
nesting_level = 0
|
82
118
|
@commands.each_with_index do |current_line, i|
|
83
119
|
index = i
|
84
120
|
commands += current_line + "\n"
|
85
|
-
|
121
|
+
if TestUnitMethod.start_def?(current_line) and TestUnitMethod.end_def?(current_line)
|
122
|
+
#let it ride
|
123
|
+
elsif TestUnitMethod.start_def?(current_line)
|
124
|
+
nesting_level += 1
|
125
|
+
elsif TestUnitMethod.end_def?(current_line)
|
126
|
+
nesting_level -= 1
|
127
|
+
elsif nesting_level == 0
|
128
|
+
eval commands #todo write tests breaking assumption that definitions are valid code, and impl separate eval's for definitions
|
129
|
+
end
|
86
130
|
end
|
87
|
-
|
131
|
+
nil
|
88
132
|
rescue Exception
|
89
133
|
# delete offender and start again from the beginning
|
90
|
-
@commands.delete_at index
|
134
|
+
@commands.delete_at index # todo: test if string equality is safe for delete()
|
135
|
+
retry
|
91
136
|
end
|
92
|
-
nil
|
93
137
|
end
|
94
138
|
|
95
139
|
# Adapted from ActiveSupport Inflector
|
data/lib/butterfly_net.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require 'butterfly_net/file_writer'
|
2
2
|
require 'butterfly_net/test_unit_method'
|
3
3
|
require 'butterfly_net/test_unit_adapter'
|
4
|
+
require 'butterfly_net/rails_test_unit_adapter'
|
5
|
+
require 'butterfly_net/test_case'
|
6
|
+
require 'butterfly_net/readline_reader'
|
4
7
|
require 'butterfly_net/commands'
|
5
8
|
|
6
9
|
include ButterflyNet::Commands
|
data/test/butterfly_net_tests.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
require "test/unit"
|
2
2
|
$: << File.expand_path(File.dirname(__FILE__))
|
3
3
|
$: << File.join(File.expand_path(File.dirname(__FILE__)), File.join("..", "lib"))
|
4
|
+
require "butterfly_net/file_writer"
|
4
5
|
require "butterfly_net/test_unit_method"
|
5
6
|
require "butterfly_net/test_unit_adapter"
|
7
|
+
require "butterfly_net/rails_test_unit_adapter"
|
8
|
+
require "butterfly_net/test_case"
|
6
9
|
|
7
|
-
class
|
10
|
+
class TestCaseTest < Test::Unit::TestCase
|
8
11
|
include ButterflyNet
|
9
12
|
|
10
13
|
def setup
|
11
|
-
@adapter =
|
14
|
+
@adapter = TestCase.new
|
12
15
|
end
|
13
16
|
|
14
17
|
def teardown
|
@@ -21,43 +24,82 @@ class TestUnitAdapterTest < Test::Unit::TestCase
|
|
21
24
|
|
22
25
|
def test_test_methods_single
|
23
26
|
@adapter.add_command("1 + 1")
|
24
|
-
expected = "def test_1\n assert_equal(2, 1 + 1)\n end"
|
27
|
+
expected = " def test_1\n assert_equal(2, 1 + 1)\n end\n\n"
|
25
28
|
assert_equal expected, @adapter.test_methods.first
|
26
29
|
end
|
27
30
|
|
28
31
|
def test_test_methods_2_assertions_1_method
|
29
32
|
@adapter.add_command("1 + 1")
|
30
33
|
@adapter.add_command("1 + 2")
|
31
|
-
expected = "def test_1\n assert_equal(2, 1 + 1)\n assert_equal(3, 1 + 2)\n end"
|
34
|
+
expected = " def test_1\n assert_equal(2, 1 + 1)\n assert_equal(3, 1 + 2)\n end\n\n"
|
32
35
|
assert_equal expected, @adapter.test_methods.last
|
33
36
|
end
|
34
37
|
|
35
38
|
def test_test_methods_variable_in_assertion
|
36
39
|
@adapter.add_command("a = 1")
|
37
40
|
@adapter.add_command("a + 2")
|
38
|
-
expected = "def test_1\n a = 1\n assert_equal(3, a + 2)\n end"
|
41
|
+
expected = " def test_1\n a = 1\n assert_equal(3, a + 2)\n end\n\n"
|
39
42
|
assert_equal expected, @adapter.test_methods.last
|
40
43
|
end
|
41
44
|
|
42
45
|
def test_test_methods_require
|
43
46
|
@adapter.add_command("require 'bigdecimal'")
|
44
47
|
@adapter.add_command("BigDecimal(\"1.0\") - 0.5")
|
45
|
-
expected = "def test_1\n require 'bigdecimal'\n assert_equal(0.5, BigDecimal(\"1.0\") - 0.5)\n end"
|
48
|
+
expected = " def test_1\n require 'bigdecimal'\n assert_equal(0.5, BigDecimal(\"1.0\") - 0.5)\n end\n\n"
|
46
49
|
assert_equal expected, @adapter.test_methods.last
|
47
50
|
end
|
48
51
|
|
49
|
-
def
|
52
|
+
def test_test_methods_def_method_single_inline
|
53
|
+
@adapter.add_command("def timestwo(i); i * 2; end")
|
54
|
+
@adapter.add_command("timestwo(4)")
|
55
|
+
expected = <<-EOF
|
56
|
+
def timestwo(i); i * 2; end
|
57
|
+
|
58
|
+
def test_1
|
59
|
+
assert_equal(8, timestwo(4))
|
60
|
+
end
|
61
|
+
|
62
|
+
EOF
|
63
|
+
assert_equal expected, @adapter.generate_bodytext
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_test_methods_def_methods_two_inline
|
67
|
+
@adapter.add_command("def timestwo(i); i * 2; end")
|
68
|
+
@adapter.add_command("def timesfour(i); timestwo(i) * timestwo(i); end")
|
69
|
+
@adapter.add_command("timestwo(1)")
|
70
|
+
@adapter.add_command("timesfour(1)")
|
71
|
+
expected = <<-EOF
|
72
|
+
def timestwo(i); i * 2; end
|
73
|
+
|
74
|
+
def timesfour(i); timestwo(i) * timestwo(i); end
|
75
|
+
|
76
|
+
def test_1
|
77
|
+
assert_equal(2, timestwo(1))
|
78
|
+
assert_equal(4, timesfour(1))
|
79
|
+
end
|
80
|
+
|
81
|
+
EOF
|
82
|
+
assert_equal expected, @adapter.generate_bodytext
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_test_methods_numbering_first_method
|
86
|
+
@adapter.add_command("1 + 1")
|
87
|
+
@adapter.close_assertion_set
|
88
|
+
@adapter.add_command("1 + 2")
|
89
|
+
assert_equal " def test_1\n assert_equal(2, 1 + 1)\n end\n\n", @adapter.test_methods.first
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_test_methods_numbering_second_method
|
50
93
|
@adapter.add_command("1 + 1")
|
51
94
|
@adapter.close_assertion_set
|
52
95
|
@adapter.add_command("1 + 2")
|
53
|
-
assert_equal "def
|
54
|
-
assert_equal "def test_2\n assert_equal(3, 1 + 2)\n end", @adapter.test_methods.last
|
96
|
+
assert_equal " def test_2\n assert_equal(3, 1 + 2)\n end\n\n", @adapter.test_methods.last
|
55
97
|
end
|
56
98
|
|
57
99
|
def test_test_methods_naming
|
58
100
|
@adapter.add_command("1 + 1")
|
59
101
|
@adapter.close_assertion_set 'test_one_plus_one'
|
60
|
-
assert_equal "def test_one_plus_one\n assert_equal(2, 1 + 1)\n end", @adapter.test_methods.first
|
102
|
+
assert_equal " def test_one_plus_one\n assert_equal(2, 1 + 1)\n end\n\n", @adapter.test_methods.first
|
61
103
|
end
|
62
104
|
|
63
105
|
def test_test_methods_bad_input
|
@@ -111,6 +111,35 @@ class TestUnitMethodTest < Test::Unit::TestCase
|
|
111
111
|
assert !TestUnitMethod.assignment_or_require?("a *= 1")
|
112
112
|
end
|
113
113
|
|
114
|
+
def test_method_def_true_whitespace
|
115
|
+
assert TestUnitMethod.start_def?(" def timestwo(i); i * 2; end ")
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_class_def_true_whitespace
|
119
|
+
assert TestUnitMethod.start_def?(" class MyClass; end ")
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_class_def_true_whitespace
|
123
|
+
assert TestUnitMethod.start_def?(" module MyModule; end ")
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_method_def_false_variable
|
127
|
+
assert !TestUnitMethod.start_def?("definite == true ")
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def test_start_def_true_inline
|
132
|
+
assert TestUnitMethod.start_def?(" def timestwo(i); i * 2; end ")
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_end_def_true_inline
|
136
|
+
assert TestUnitMethod.end_def?(" def timestwo(i); i * 2; end ")
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_end_def_true_whitespace
|
140
|
+
assert TestUnitMethod.end_def?(" end ")
|
141
|
+
end
|
142
|
+
|
114
143
|
def test_name_uppercase
|
115
144
|
@method.name = "MYMETHOD"
|
116
145
|
assert_equal("test_mymethod", @method.name)
|
@@ -152,21 +181,21 @@ class TestUnitMethodTest < Test::Unit::TestCase
|
|
152
181
|
@method << "a = 1"
|
153
182
|
@method << "2b" # syntax error, unexpected tIDENTIFIER, expecting $end
|
154
183
|
@method << "a += 1"
|
155
|
-
assert_equal "def test_1\n a = 1\n assert_equal(2, a += 1)\n end", @method.text
|
184
|
+
assert_equal " def test_1\n a = 1\n assert_equal(2, a += 1)\n end\n\n", @method.text
|
156
185
|
end
|
157
186
|
|
158
187
|
def test_purge_unexpected_identifiers
|
159
188
|
@method << "a += 1" # undefined method `+' for nil:NilClass
|
160
189
|
@method << "a = 1"
|
161
190
|
@method << "a += 1"
|
162
|
-
assert_equal "def test_1\n a = 1\n assert_equal(2, a += 1)\n end", @method.text
|
191
|
+
assert_equal " def test_1\n a = 1\n assert_equal(2, a += 1)\n end\n\n", @method.text
|
163
192
|
end
|
164
193
|
|
165
194
|
def test_text_require
|
166
195
|
@method << "require 'rubygems'"
|
167
196
|
@method << "require'active_support'"
|
168
197
|
@method << "'CamelCase'.underscore"
|
169
|
-
assert_equal "def test_1\n require 'rubygems'\n require'active_support'\n assert_equal(\"camel_case\", 'CamelCase'.underscore)\n end", @method.text
|
198
|
+
assert_equal " def test_1\n require 'rubygems'\n require'active_support'\n assert_equal(\"camel_case\", 'CamelCase'.underscore)\n end\n\n", @method.text
|
170
199
|
end
|
171
200
|
|
172
201
|
def test_expected_boolean
|
@@ -236,6 +265,13 @@ class TestUnitMethodTest < Test::Unit::TestCase
|
|
236
265
|
assert_equal("assert_equal([1], #{line})", @method.assertion(0))
|
237
266
|
end
|
238
267
|
|
268
|
+
def test_text_def_method_single_line
|
269
|
+
@method << "def timestwo(i); i * 2; end"
|
270
|
+
line = "timestwo(2)"
|
271
|
+
@method << line
|
272
|
+
assert_equal(" def timestwo(i); i * 2; end\n\n def test_1\n assert_equal(4, #{line})\n end\n\n", @method.text)
|
273
|
+
end
|
274
|
+
|
239
275
|
def test_assertion_illegal_input
|
240
276
|
line = "badtext"
|
241
277
|
@method << line
|
@@ -244,29 +280,39 @@ class TestUnitMethodTest < Test::Unit::TestCase
|
|
244
280
|
|
245
281
|
def test_text
|
246
282
|
@method << "1 + 1"
|
247
|
-
assert_equal "def test_1\n assert_equal(2, 1 + 1)\n end", @method.text
|
283
|
+
assert_equal " def test_1\n assert_equal(2, 1 + 1)\n end\n\n", @method.text
|
248
284
|
end
|
249
285
|
|
286
|
+
def test_text_variable_assignment_only
|
287
|
+
line = "a = 1"
|
288
|
+
@method << line
|
289
|
+
assert_equal " def test_1\n #{line}\n end\n\n", @method.text
|
290
|
+
end
|
250
291
|
|
292
|
+
def test_extract_definitions_class_multiline_empty
|
293
|
+
line = "class MyClass"
|
294
|
+
line2 = "end"
|
295
|
+
line3 = "MyClass.new"
|
296
|
+
@method << line
|
297
|
+
@method << line2
|
298
|
+
@method << line3
|
299
|
+
assert_equal " #{line}\n #{line2}\n\n def test_1\n assert_not_equal((#{line3}), #{line3})\n end\n\n", @method.text
|
300
|
+
end
|
301
|
+
|
302
|
+
def test_extract_definitions_class_with_method
|
303
|
+
@method << "class MyClass"
|
304
|
+
@method << "def name"
|
305
|
+
@method << "\"classy\""
|
306
|
+
@method << "end"
|
307
|
+
@method << "end"
|
308
|
+
@method << "MyClass.new.name"
|
309
|
+
assert_equal " class MyClass\n def name\n \"classy\"\n end\n end\n\n def test_1\n assert_equal(\"classy\", MyClass.new.name)\n end\n\n", @method.text
|
310
|
+
end
|
251
311
|
|
252
312
|
def test_text_bad_input_constant
|
253
313
|
@method << "BADCONSTANT"
|
254
314
|
assert_nil @method.text
|
255
315
|
end
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
316
|
# non-project tests (scratchpad)
|
271
317
|
|
272
318
|
def test_eval_scope
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: butterfly_net
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Smith
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-24 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,11 +25,14 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- lib/butterfly_net/commands.rb
|
27
27
|
- lib/butterfly_net/file_writer.rb
|
28
|
+
- lib/butterfly_net/rails_test_unit_adapter.rb
|
29
|
+
- lib/butterfly_net/readline_reader.rb
|
30
|
+
- lib/butterfly_net/test_case.rb
|
28
31
|
- lib/butterfly_net/test_unit_adapter.rb
|
29
32
|
- lib/butterfly_net/test_unit_method.rb
|
30
33
|
- lib/butterfly_net.rb
|
31
34
|
- test/butterfly_net_tests.rb
|
32
|
-
- test/
|
35
|
+
- test/test_case_test.rb
|
33
36
|
- test/test_unit_method_test.rb
|
34
37
|
has_rdoc: true
|
35
38
|
homepage: http://www.butterflynet.org
|