butterfly_net 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.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
|