mattwynne-cucover 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +7 -3
- data/bin/cucover +0 -2
- data/lib/cucover.rb +167 -88
- metadata +1 -1
data/README.markdown
CHANGED
@@ -10,11 +10,14 @@ How does it decide whether it needs to run a feature? Every time you run a featu
|
|
10
10
|
|
11
11
|
* Uses RCov to map features to covered source files
|
12
12
|
* Patches Rails to also map features to covered .erb templates
|
13
|
+
* Shows skipped Scenarios, for confidence
|
14
|
+
* Re-runs failing features, even when nothing has changed, for that good old red-bar feel.
|
13
15
|
|
14
16
|
## Installation and Usage
|
15
17
|
|
16
18
|
Something like this, as I haven't figured out the dependencies yet for the gem:
|
17
19
|
|
20
|
+
sudo gem install cucumber
|
18
21
|
sudo gem install spicycode-rcov
|
19
22
|
sudo gem install mattwynne-cucover
|
20
23
|
|
@@ -26,8 +29,10 @@ To run your features lazily, just use the cucover binary instead of cucumber. Us
|
|
26
29
|
* This is very new and experimental. There may be bugs. Feedback is welcome via github messages.
|
27
30
|
|
28
31
|
## Todo
|
29
|
-
*
|
30
|
-
*
|
32
|
+
* One or two of the features for Cucuover itself seem to flicker (intermittently fail). This is probably due to timing issues when figuring out if a file is dirty.
|
33
|
+
* Make laziness work down to the granularity of scenarios, rather than features
|
34
|
+
* I suspect it does wierd things if you pass more than one visitor. Need to test for this.
|
35
|
+
* Speed up the Rails test - maybe strip some guff out of the environment load?
|
31
36
|
|
32
37
|
## Similar 'Selective Testing' Tools
|
33
38
|
|
@@ -45,4 +50,3 @@ To run your features lazily, just use the cucover binary instead of cucumber. Us
|
|
45
50
|
* JUnitMax
|
46
51
|
* a selective testing tool by [Kent Beck](http://www.threeriversinstitute.org/blog)
|
47
52
|
* http://junitmax.com/
|
48
|
-
|
data/bin/cucover
CHANGED
data/lib/cucover.rb
CHANGED
@@ -1,92 +1,81 @@
|
|
1
|
-
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
-
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
1
|
|
4
2
|
require 'rubygems'
|
3
|
+
|
4
|
+
gem 'cucumber', '>=0.3'
|
5
|
+
require 'cucumber'
|
6
|
+
|
7
|
+
gem 'spicycode-rcov', '>=0.8.1.5.0'
|
5
8
|
require 'rcov'
|
9
|
+
require 'spec'
|
10
|
+
|
11
|
+
$:.unshift(File.dirname(__FILE__))
|
12
|
+
require 'cucover/monkey'
|
13
|
+
require 'cucover/rails'
|
6
14
|
|
7
15
|
module Cucover
|
8
16
|
|
9
|
-
class
|
10
|
-
def
|
11
|
-
|
12
|
-
end
|
13
|
-
def additional_covered_files
|
14
|
-
@additional_covered_files ||= []
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
class SourceFileCache
|
19
|
-
def initialize(feature_file)
|
20
|
-
@feature_file = feature_file
|
17
|
+
class TestRun
|
18
|
+
def initialize(feature_file, visitor)
|
19
|
+
@feature_file, @visitor = feature_file, visitor
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
24
|
-
|
25
|
-
File.open(cache_filename, "w") do |file|
|
26
|
-
file.puts analyzed_files
|
27
|
-
end
|
22
|
+
def record(source_file)
|
23
|
+
additional_covered_files << source_file
|
28
24
|
end
|
29
25
|
|
30
|
-
def
|
31
|
-
|
26
|
+
def fail!
|
27
|
+
@failed = true
|
32
28
|
end
|
33
29
|
|
34
|
-
def
|
35
|
-
|
30
|
+
def watch
|
31
|
+
announce_skip unless may_execute?
|
32
|
+
|
33
|
+
analyzer.run_hooked do
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
|
37
|
+
record(@feature_file)
|
38
|
+
source_files_cache.save analyzed_files
|
39
|
+
status_cache.record(status)
|
36
40
|
end
|
37
41
|
|
38
|
-
def
|
39
|
-
|
42
|
+
def may_execute?
|
43
|
+
dirty? || failed_on_last_run?
|
40
44
|
end
|
41
|
-
|
45
|
+
|
42
46
|
private
|
43
|
-
|
44
|
-
def
|
45
|
-
|
46
|
-
File.mtime(Dir.pwd + '/' + source_file.strip) >= time
|
47
|
-
end
|
47
|
+
|
48
|
+
def status
|
49
|
+
@failed ? :failed : :passed
|
48
50
|
end
|
49
51
|
|
50
|
-
def
|
51
|
-
|
52
|
-
File.open(cache_filename, "r") do |file|
|
53
|
-
file.each_line do |line|
|
54
|
-
result.push line
|
55
|
-
end
|
56
|
-
end
|
57
|
-
result
|
52
|
+
def additional_covered_files
|
53
|
+
@additional_covered_files ||= []
|
58
54
|
end
|
59
55
|
|
60
|
-
def
|
61
|
-
|
56
|
+
def announce_skip
|
57
|
+
messages = []
|
58
|
+
messages << "Cucover - Skipping clean feature"
|
59
|
+
messages << "Last run status: #{status_cache.last_run_status}"
|
60
|
+
@visitor.announce messages.flatten.map{ |m| "[ #{m.rstrip} ]"}.join("\n")
|
62
61
|
end
|
63
|
-
end
|
64
|
-
|
65
|
-
module LazyFeature
|
66
62
|
|
67
|
-
def
|
68
|
-
return unless
|
69
|
-
|
70
|
-
analyzer.run_hooked do
|
71
|
-
super
|
72
|
-
end
|
73
|
-
source_files_cache.save analyzed_files
|
63
|
+
def failed_on_last_run?
|
64
|
+
return false unless status_cache.exists?
|
65
|
+
status_cache.last_run_status == "failed"
|
74
66
|
end
|
75
|
-
|
76
|
-
private
|
77
67
|
|
78
68
|
def dirty?
|
79
69
|
return true unless source_files_cache.exists?
|
80
|
-
return true if changed_since_last_run?
|
81
70
|
source_files_cache.any_dirty_files?
|
82
71
|
end
|
83
72
|
|
84
|
-
def changed_since_last_run?
|
85
|
-
File.mtime(@file) >= source_files_cache.time
|
86
|
-
end
|
87
|
-
|
88
73
|
def source_files_cache
|
89
|
-
@source_files_cache ||= SourceFileCache.new(@
|
74
|
+
@source_files_cache ||= SourceFileCache.new(@feature_file)
|
75
|
+
end
|
76
|
+
|
77
|
+
def status_cache
|
78
|
+
@status_cache ||= StatusCache.new(@feature_file)
|
90
79
|
end
|
91
80
|
|
92
81
|
def source_files
|
@@ -98,61 +87,151 @@ module Cucover
|
|
98
87
|
end
|
99
88
|
|
100
89
|
def normalized_files
|
101
|
-
(analyzer.analyzed_files +
|
90
|
+
(analyzer.analyzed_files + additional_covered_files.uniq).map{ |f| File.expand_path(f).gsub(/^#{Dir.pwd}\//, '') }
|
102
91
|
end
|
103
92
|
|
104
93
|
def boring?(file)
|
94
|
+
return false
|
105
95
|
(file.match /gem/) || (file.match /vendor/) || (file.match /lib\/ruby/)
|
106
96
|
end
|
107
97
|
|
108
98
|
def analyzer
|
109
99
|
@analyzer ||= Rcov::CodeCoverageAnalyzer.new
|
110
100
|
end
|
101
|
+
|
111
102
|
end
|
112
103
|
|
113
|
-
|
114
|
-
def
|
115
|
-
|
104
|
+
class << self
|
105
|
+
def start_test(test, visitor)
|
106
|
+
@current_test = TestRun.new(test.file, visitor)
|
107
|
+
|
108
|
+
@current_test.watch do
|
109
|
+
yield
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def fail_current_test!
|
114
|
+
current_test.fail!
|
115
|
+
end
|
116
|
+
|
117
|
+
def record(source_file)
|
118
|
+
current_test.record(source_file)
|
119
|
+
end
|
120
|
+
|
121
|
+
def should_skip?
|
122
|
+
not current_test.may_execute?
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def current_test
|
128
|
+
@current_test or raise("You need to start the a test first!")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class Cache
|
133
|
+
def initialize(feature_file)
|
134
|
+
@feature_file = feature_file
|
135
|
+
end
|
136
|
+
|
137
|
+
def exists?
|
138
|
+
File.exist?(cache_filename)
|
139
|
+
end
|
140
|
+
|
141
|
+
def cache_filename
|
142
|
+
@feature_file.gsub /([^\/]*\.feature)/, '.coverage/\1'
|
143
|
+
end
|
144
|
+
|
145
|
+
def time
|
146
|
+
File.mtime(cache_filename)
|
147
|
+
end
|
148
|
+
|
149
|
+
def write_to_cache
|
150
|
+
FileUtils.mkdir_p File.dirname(cache_filename)
|
151
|
+
File.open(cache_filename, "w") do |file|
|
152
|
+
yield file
|
153
|
+
end
|
116
154
|
end
|
117
155
|
end
|
118
156
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
ActionView::Template.instance_eval do
|
126
|
-
def new(*args)
|
127
|
-
super(*args).extend(Cucover::Rails::RecordsRenders)
|
128
|
-
end
|
157
|
+
class StatusCache < Cache
|
158
|
+
def last_run_status
|
159
|
+
File.open(cache_filename, "r") do |file|
|
160
|
+
file.each_line do |line|
|
161
|
+
return line.strip
|
129
162
|
end
|
130
|
-
|
131
|
-
@patched = true
|
132
163
|
end
|
133
164
|
end
|
134
165
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
super
|
166
|
+
def record(status)
|
167
|
+
write_to_cache do |file|
|
168
|
+
file.puts status
|
139
169
|
end
|
140
170
|
end
|
141
|
-
|
142
|
-
|
171
|
+
|
172
|
+
private
|
143
173
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
174
|
+
def cache_filename
|
175
|
+
super + '.status'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class SourceFileCache < Cache
|
180
|
+
def save(analyzed_files)
|
181
|
+
write_to_cache do |file|
|
182
|
+
file.puts analyzed_files
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def any_dirty_files?
|
187
|
+
not dirty_files.empty?
|
188
|
+
end
|
189
|
+
|
190
|
+
def source_files
|
191
|
+
result = []
|
192
|
+
File.open(cache_filename, "r") do |file|
|
193
|
+
file.each_line do |line|
|
194
|
+
result.push line
|
150
195
|
end
|
151
196
|
end
|
197
|
+
result
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def dirty_files
|
203
|
+
source_files.select do |source_file|
|
204
|
+
File.mtime(source_file.strip) >= time
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
module LazyStepInvocation
|
211
|
+
def accept(visitor)
|
212
|
+
skip_invoke! if Cucover.should_skip?
|
213
|
+
super
|
214
|
+
end
|
215
|
+
|
216
|
+
def failed(exception, clear_backtrace)
|
217
|
+
Cucover.fail_current_test!
|
218
|
+
super
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
module LazyFeature
|
223
|
+
def accept(visitor)
|
224
|
+
Cucover.start_test(self, visitor) do
|
225
|
+
super
|
226
|
+
end
|
152
227
|
end
|
228
|
+
|
153
229
|
end
|
154
230
|
end
|
155
231
|
|
232
|
+
Cucover::Monkey.extend_every Cucumber::Ast::Feature => Cucover::LazyFeature
|
233
|
+
Cucover::Monkey.extend_every Cucumber::Ast::StepInvocation => Cucover::LazyStepInvocation
|
234
|
+
|
156
235
|
Before do
|
157
236
|
Cucover::Rails.patch_if_necessary
|
158
|
-
end
|
237
|
+
end
|