butternut 0.1.0 → 0.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/Rakefile +2 -1
- data/VERSION +1 -1
- data/butternut.gemspec +3 -4
- data/lib/butternut/formatter.rb +87 -420
- data/lib/butternut.rb +8 -13
- data/spec/butternut/formatter_spec.rb +84 -256
- data/spec/fixtures/foo.html +1 -0
- data/spec/spec.opts +0 -1
- data/spec/spec_helper.rb +20 -1
- data/tmp/{features/.gitignore → .gitignore} +0 -0
- metadata +3 -4
- data/tmp/main/.gitignore +0 -2
data/Rakefile
CHANGED
@@ -23,6 +23,7 @@ end
|
|
23
23
|
|
24
24
|
require 'spec/rake/spectask'
|
25
25
|
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.ruby_opts = %w{-X+O}
|
26
27
|
spec.libs << 'lib' << 'spec'
|
27
28
|
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
29
|
end
|
@@ -56,6 +57,6 @@ namespace :tmp do
|
|
56
57
|
desc 'Delete temporary files'
|
57
58
|
task :clear do
|
58
59
|
require 'fileutils'
|
59
|
-
FileUtils.rm_rf(Dir.glob(File.dirname(__FILE__) + "/tmp
|
60
|
+
FileUtils.rm_rf(Dir.glob(File.dirname(__FILE__) + "/tmp/*"))
|
60
61
|
end
|
61
62
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/butternut.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{butternut}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jeremy Stephens"]
|
12
|
-
s.date = %q{2009-
|
12
|
+
s.date = %q{2009-12-14}
|
13
13
|
s.description = %q{Based on Cucumber's HTML formatter, Butternut uses Celerity to capture page sources after each step.}
|
14
14
|
s.email = %q{viking415@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -41,8 +41,7 @@ Gem::Specification.new do |s|
|
|
41
41
|
"spec/fixtures/picard.jpg",
|
42
42
|
"spec/spec.opts",
|
43
43
|
"spec/spec_helper.rb",
|
44
|
-
"tmp
|
45
|
-
"tmp/main/.gitignore"
|
44
|
+
"tmp/.gitignore"
|
46
45
|
]
|
47
46
|
s.homepage = %q{http://github.com/viking/butternut}
|
48
47
|
s.rdoc_options = ["--charset=UTF-8"]
|
data/lib/butternut/formatter.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
require 'cucumber/formatter/
|
2
|
-
require 'cucumber/formatter/duration'
|
1
|
+
require 'cucumber/formatter/html'
|
3
2
|
|
4
|
-
require 'tmpdir'
|
5
3
|
require 'fileutils'
|
6
4
|
require 'nokogiri'
|
7
5
|
require 'uri'
|
@@ -9,467 +7,136 @@ require 'open-uri'
|
|
9
7
|
require 'net/ftp' # For Net::FTPPermError
|
10
8
|
|
11
9
|
module Butternut
|
12
|
-
class Formatter
|
13
|
-
include ERB::Util # for the #h method
|
14
|
-
include Cucumber::Formatter::Duration
|
15
|
-
|
16
|
-
# FIXME: this is obviously not nominal, but I have no way of
|
17
|
-
# accepting additional options from Cucumber at present
|
18
|
-
FEATURES_DIR_PARTS = %w{.. features}
|
19
|
-
FEATURES_HTML_PREFIX = "/features"
|
10
|
+
class Formatter < Cucumber::Formatter::Html
|
20
11
|
|
21
12
|
def initialize(step_mother, io, options)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if @options && @options[:formats]
|
28
|
-
format = @options[:formats].detect { |(name, _)| underscore(name) == "butternut/formatter" }
|
29
|
-
if format && format[1].is_a?(String)
|
30
|
-
base_dir = File.dirname(File.expand_path(format[1]))
|
31
|
-
features_dir = File.expand_path(File.join(base_dir, *FEATURES_DIR_PARTS))
|
32
|
-
|
33
|
-
if File.exist?(features_dir)
|
34
|
-
today = Date.today.to_s
|
35
|
-
|
36
|
-
@source_output_dir = File.join(features_dir, today)
|
37
|
-
@source_html_path = FEATURES_HTML_PREFIX + "/" + today
|
38
|
-
if !File.exist?(@source_output_dir)
|
39
|
-
FileUtils.mkdir(@source_output_dir)
|
40
|
-
end
|
41
|
-
else
|
42
|
-
$stderr.puts "Directory not found: #{features_dir}"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
@source_output_dir ||= Dir.tmpdir
|
47
|
-
@source_html_path ||= "file://" + Dir.tmpdir
|
48
|
-
end
|
49
|
-
|
50
|
-
def before_features(features)
|
51
|
-
start_buffering :features
|
52
|
-
end
|
53
|
-
|
54
|
-
def after_features(features)
|
55
|
-
stop_buffering :features
|
56
|
-
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
57
|
-
builder.declare!(
|
58
|
-
:DOCTYPE,
|
59
|
-
:html,
|
60
|
-
:PUBLIC,
|
61
|
-
'-//W3C//DTD XHTML 1.0 Strict//EN',
|
62
|
-
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
|
63
|
-
)
|
64
|
-
builder.html(:xmlns => 'http://www.w3.org/1999/xhtml') do
|
65
|
-
builder.head do
|
66
|
-
builder.meta(:content => 'text/html;charset=utf-8')
|
67
|
-
builder.title 'Cucumber'
|
68
|
-
inline_css
|
69
|
-
end
|
70
|
-
builder.body do
|
71
|
-
builder.div(:class => 'cucumber') do
|
72
|
-
builder << buffer(:features)
|
73
|
-
builder.div(format_duration(features.duration), :class => 'duration')
|
74
|
-
end
|
75
|
-
end
|
13
|
+
# find the format options
|
14
|
+
format = options[:formats].detect { |(name, _)| name == "Butternut::Formatter" }
|
15
|
+
if !format || !format[1].is_a?(String)
|
16
|
+
raise "Butternut::Formatter cannot output to STDOUT"
|
76
17
|
end
|
77
|
-
|
78
|
-
|
79
|
-
def before_feature(feature)
|
80
|
-
start_buffering :feature
|
81
|
-
@exceptions = []
|
82
|
-
end
|
18
|
+
out = format[1]
|
83
19
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
def after_comment(comment)
|
96
|
-
stop_buffering :comment
|
97
|
-
builder.pre(:class => 'comment') do
|
98
|
-
builder << buffer(:comment)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def comment_line(comment_line)
|
103
|
-
builder.text!(comment_line)
|
104
|
-
builder.br
|
105
|
-
end
|
106
|
-
|
107
|
-
def after_tags(tags)
|
108
|
-
@tag_spacer = nil
|
109
|
-
end
|
110
|
-
|
111
|
-
def tag_name(tag_name)
|
112
|
-
builder.text!(@tag_spacer) if @tag_spacer
|
113
|
-
@tag_spacer = ' '
|
114
|
-
builder.span(tag_name, :class => 'tag')
|
115
|
-
end
|
116
|
-
|
117
|
-
def feature_name(name)
|
118
|
-
lines = name.split(/\r?\n/)
|
119
|
-
return if lines.empty?
|
120
|
-
builder.h2 do |h2|
|
121
|
-
builder.span(lines[0], :class => 'val')
|
122
|
-
end
|
123
|
-
builder.p(:class => 'narrative') do
|
124
|
-
lines[1..-1].each do |line|
|
125
|
-
builder.text!(line.strip)
|
126
|
-
builder.br
|
20
|
+
super
|
21
|
+
if File.directory?(out)
|
22
|
+
#@assets_dir = out
|
23
|
+
#@assets_url = "."
|
24
|
+
else
|
25
|
+
basename = File.basename(out).sub(/\..*$/, "")
|
26
|
+
@assets_dir = File.join(File.dirname(out), basename)
|
27
|
+
@assets_url = basename
|
28
|
+
if !File.exist?(@assets_dir)
|
29
|
+
FileUtils.mkdir(@assets_dir)
|
127
30
|
end
|
128
31
|
end
|
129
32
|
end
|
130
33
|
|
131
|
-
def before_background(background)
|
132
|
-
@in_background = true
|
133
|
-
start_buffering :background
|
134
|
-
end
|
135
|
-
|
136
|
-
def after_background(background)
|
137
|
-
stop_buffering :background
|
138
|
-
@in_background = nil
|
139
|
-
builder.div(:class => 'background') do
|
140
|
-
builder << buffer(:background)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def background_name(keyword, name, file_colon_line, source_indent)
|
145
|
-
@listing_background = true
|
146
|
-
builder.h3 do |h3|
|
147
|
-
builder.span(keyword, :class => 'keyword')
|
148
|
-
builder.text!(' ')
|
149
|
-
builder.span(name, :class => 'val')
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
34
|
def before_feature_element(feature_element)
|
154
|
-
|
35
|
+
super
|
155
36
|
@feature_element = feature_element
|
156
37
|
end
|
157
38
|
|
158
|
-
def after_feature_element(feature_element)
|
159
|
-
stop_buffering :feature_element
|
160
|
-
css_class = {
|
161
|
-
Cucumber::Ast::Scenario => 'scenario',
|
162
|
-
Cucumber::Ast::ScenarioOutline => 'scenario outline'
|
163
|
-
}[feature_element.class]
|
164
|
-
|
165
|
-
builder.div(:class => css_class) do
|
166
|
-
builder << buffer(:feature_element)
|
167
|
-
end
|
168
|
-
@open_step_list = true
|
169
|
-
@feature_element = nil
|
170
|
-
end
|
171
|
-
|
172
|
-
def scenario_name(keyword, name, file_colon_line, source_indent)
|
173
|
-
@listing_background = false
|
174
|
-
builder.h3 do
|
175
|
-
builder.span(keyword, :class => 'keyword')
|
176
|
-
builder.text!(' ')
|
177
|
-
builder.span(name, :class => 'val')
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def before_outline_table(outline_table)
|
182
|
-
@outline_row = 0
|
183
|
-
start_buffering :outline_table
|
184
|
-
end
|
185
|
-
|
186
|
-
def after_outline_table(outline_table)
|
187
|
-
stop_buffering :outline_table
|
188
|
-
builder.table do
|
189
|
-
builder << buffer(:outline_table)
|
190
|
-
end
|
191
|
-
@outline_row = nil
|
192
|
-
end
|
193
|
-
|
194
|
-
def before_examples(examples)
|
195
|
-
start_buffering :examples
|
196
|
-
end
|
197
|
-
|
198
|
-
def after_examples(examples)
|
199
|
-
stop_buffering :examples
|
200
|
-
builder.div(:class => 'examples') do
|
201
|
-
builder << buffer(:examples)
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def examples_name(keyword, name)
|
206
|
-
builder.h4 do
|
207
|
-
builder.span(keyword, :class => 'keyword')
|
208
|
-
builder.text!(' ')
|
209
|
-
builder.span(name, :class => 'val')
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
def before_steps(steps)
|
214
|
-
start_buffering :steps
|
215
|
-
end
|
216
|
-
|
217
|
-
def after_steps(steps)
|
218
|
-
stop_buffering :steps
|
219
|
-
builder.ol do
|
220
|
-
builder << buffer(:steps)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
def before_step(step)
|
225
|
-
@step_id = step.dom_id
|
226
|
-
end
|
227
|
-
|
228
|
-
def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
|
229
|
-
start_buffering :step_result
|
230
|
-
@hide_this_step = false
|
231
|
-
if exception
|
232
|
-
if @exceptions.include?(exception)
|
233
|
-
@hide_this_step = true
|
234
|
-
return
|
235
|
-
end
|
236
|
-
@exceptions << exception
|
237
|
-
end
|
238
|
-
if status != :failed && @in_background ^ background
|
239
|
-
@hide_this_step = true
|
240
|
-
return
|
241
|
-
end
|
242
|
-
@status = status
|
243
|
-
end
|
244
|
-
|
245
39
|
def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
|
246
40
|
stop_buffering :step_result
|
247
41
|
return if @hide_this_step
|
248
42
|
builder.li(:id => @step_id, :class => "step #{status}") do
|
249
|
-
builder
|
250
|
-
|
251
|
-
end
|
252
|
-
builder.div(:class => "page") do
|
253
|
-
if @feature_element.respond_to?(:last_page_source)
|
254
|
-
page_source = @feature_element.last_page_source
|
255
|
-
if page_source
|
256
|
-
page_url = @feature_element.last_page_url
|
257
|
-
page_source = transform_page_source(page_source, page_url)
|
258
|
-
path = source_file_name
|
259
|
-
File.open(path, "w") { |f| f.print(page_source) }
|
260
|
-
|
261
|
-
builder.a({:target => "_blank", :href => "#{@source_html_path}/#{File.basename(path)}"}) do
|
262
|
-
builder.span(:class => "icon-show") { builder << "" }
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|
43
|
+
add_page_source_link(builder)
|
44
|
+
builder << buffer(:step_result)
|
267
45
|
end
|
268
46
|
end
|
269
47
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
unless @skip_step
|
277
|
-
build_step(keyword, step_match, status)
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
def exception(exception, status)
|
282
|
-
return if @hide_this_step
|
283
|
-
builder.pre(format_exception(exception), :class => status)
|
284
|
-
end
|
285
|
-
|
286
|
-
def before_multiline_arg(multiline_arg)
|
287
|
-
start_buffering :multiline_arg
|
288
|
-
end
|
289
|
-
|
290
|
-
def after_multiline_arg(multiline_arg)
|
291
|
-
stop_buffering :multiline_arg
|
292
|
-
return if @hide_this_step || @skip_step
|
293
|
-
if Cucumber::Ast::Table === multiline_arg
|
294
|
-
builder.table do
|
295
|
-
builder << buffer(:multiline_arg)
|
48
|
+
private
|
49
|
+
def add_page_source_link(builder)
|
50
|
+
if !@feature_element.respond_to?(:last_page_source) || @feature_element.last_page_source.nil?
|
51
|
+
# don't add a link of we haven't interacted with a webpage
|
52
|
+
return
|
296
53
|
end
|
297
|
-
else
|
298
|
-
builder << buffer(:multiline_arg)
|
299
|
-
end
|
300
|
-
end
|
301
54
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
end
|
307
|
-
end
|
55
|
+
page_source = @feature_element.last_page_source
|
56
|
+
page_url = @feature_element.last_page_url
|
57
|
+
@feature_element.last_page_source = nil
|
58
|
+
@feature_element.last_page_url = nil
|
308
59
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
start_buffering :table_row
|
313
|
-
end
|
60
|
+
page_source = transform_page_source(page_source, page_url)
|
61
|
+
path = source_file_name
|
62
|
+
File.open(path, "w") { |f| f.print(page_source) }
|
314
63
|
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
builder.table(:id => @row_id) do
|
319
|
-
builder << buffer(:table_row)
|
320
|
-
end
|
321
|
-
if table_row.exception
|
322
|
-
builder.tr do
|
323
|
-
builder.td(:colspan => @col_index.to_s, :class => 'failed') do
|
324
|
-
builder.pre do |pre|
|
325
|
-
pre << format_exception(table_row.exception)
|
326
|
-
end
|
64
|
+
builder.div(:style => "float: right") do
|
65
|
+
builder.a({:target => "_blank", :href => "#{@assets_url}/#{File.basename(path)}"}) do
|
66
|
+
builder << "Source"
|
327
67
|
end
|
328
68
|
end
|
329
69
|
end
|
330
|
-
@outline_row += 1 if @outline_row
|
331
|
-
end
|
332
|
-
|
333
|
-
def table_cell_value(value, status)
|
334
|
-
return if @hide_this_step
|
335
|
-
|
336
|
-
cell_type = @outline_row == 0 ? :th : :td
|
337
|
-
attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'val'}
|
338
|
-
attributes[:class] += " #{status}" if status
|
339
|
-
build_cell(cell_type, value, attributes)
|
340
|
-
@col_index += 1
|
341
|
-
end
|
342
70
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
def build_step(keyword, step_match, status)
|
350
|
-
step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
|
351
|
-
builder.div do |div|
|
352
|
-
builder.span(keyword, :class => 'keyword')
|
353
|
-
builder.text!(' ')
|
354
|
-
builder.span(:class => 'step val') do |name|
|
355
|
-
name << h(step_name).gsub(/<span class="(.*?)">/, '<span class="\1">').gsub(/<\/span>/, '</span>')
|
71
|
+
def source_file_name
|
72
|
+
t = Time.now.strftime("%Y%m%d")
|
73
|
+
path = nil
|
74
|
+
while path.nil?
|
75
|
+
path = File.join(@assets_dir, "butternut#{t}-#{$$}-#{rand(0x100000000).to_s(36)}.html")
|
76
|
+
path = nil if File.exist?(path)
|
356
77
|
end
|
78
|
+
path
|
357
79
|
end
|
358
|
-
end
|
359
80
|
|
360
|
-
|
361
|
-
|
362
|
-
|
81
|
+
def transform_page_source(page_source, page_url)
|
82
|
+
base_uri = URI.parse(page_url)
|
83
|
+
base_uri.query = nil
|
84
|
+
@already_collected = []
|
363
85
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
(["#{exception.message} (#{exception.class})"] + exception.backtrace).join("\n")
|
372
|
-
end
|
86
|
+
doc = Nokogiri.HTML(page_source)
|
87
|
+
{ :image => ['img', 'src'],
|
88
|
+
:stylesheet => ['link[rel=stylesheet]', 'href']
|
89
|
+
}.each_pair do |type, (selector, attr)|
|
90
|
+
doc.css(selector).each do |elt|
|
91
|
+
elt_url = elt[attr]
|
92
|
+
next if elt_url.nil? || elt_url.empty?
|
373
93
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
def buffer(label)
|
379
|
-
result = @buffer[label]
|
380
|
-
@buffer[label] = ''
|
381
|
-
result
|
382
|
-
end
|
94
|
+
result = save_remote_file(base_uri, type, elt_url)
|
95
|
+
elt[attr] = result if result
|
96
|
+
end
|
97
|
+
end
|
383
98
|
|
384
|
-
|
385
|
-
|
386
|
-
@parent_builder ||= {}
|
387
|
-
@parent_builder[label] = @current_builder
|
388
|
-
@current_builder = create_builder(@buffer[label])
|
389
|
-
end
|
99
|
+
# disable links
|
100
|
+
doc.css('a').each { |link| link['href'] = "#" }
|
390
101
|
|
391
|
-
|
392
|
-
|
393
|
-
end
|
102
|
+
# turn off scripts
|
103
|
+
doc.css('script').each { |s| s.unlink }
|
394
104
|
|
395
|
-
|
396
|
-
|
397
|
-
end
|
105
|
+
# disable form elements
|
106
|
+
doc.css('input, select, textarea').each { |x| x['disabled'] = 'disabled' }
|
398
107
|
|
399
|
-
|
400
|
-
t = Time.now.strftime("%Y%m%d")
|
401
|
-
path = nil
|
402
|
-
while path.nil?
|
403
|
-
path = File.join(@source_output_dir, "butternut#{t}-#{$$}-#{rand(0x100000000).to_s(36)}.html")
|
404
|
-
path = nil if File.exist?(path)
|
108
|
+
doc.to_s
|
405
109
|
end
|
406
|
-
path
|
407
|
-
end
|
408
|
-
|
409
|
-
# Snagged from active_support
|
410
|
-
def underscore(camel_cased_word)
|
411
|
-
camel_cased_word.to_s.gsub(/::/, '/').
|
412
|
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
413
|
-
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
414
|
-
tr("-", "_").
|
415
|
-
downcase
|
416
|
-
end
|
417
|
-
|
418
|
-
def transform_page_source(page_source, page_url)
|
419
|
-
base_uri = URI.parse(page_url)
|
420
|
-
base_uri.query = nil
|
421
|
-
@already_collected = []
|
422
|
-
|
423
|
-
doc = Nokogiri.HTML(page_source)
|
424
|
-
{ :image => ['img', 'src'],
|
425
|
-
:stylesheet => ['link[rel=stylesheet]', 'href']
|
426
|
-
}.each_pair do |type, (selector, attr)|
|
427
|
-
doc.css(selector).each do |elt|
|
428
|
-
elt_url = elt[attr]
|
429
|
-
next if elt_url.nil? || elt_url.empty?
|
430
110
|
|
431
|
-
|
432
|
-
|
111
|
+
def transform_stylesheet(stylesheet_uri, content)
|
112
|
+
content.gsub(%r{url\(([^\)]+)\)}) do |_|
|
113
|
+
result = save_remote_file(stylesheet_uri, :image, $1)
|
114
|
+
"url(#{result || $1})"
|
433
115
|
end
|
434
116
|
end
|
435
117
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
# turn off scripts
|
440
|
-
doc.css('script').each { |s| s.unlink }
|
441
|
-
|
442
|
-
# disable form elements
|
443
|
-
doc.css('input, select, textarea').each { |x| x['disabled'] = 'disabled' }
|
444
|
-
|
445
|
-
doc.to_s
|
446
|
-
end
|
447
|
-
|
448
|
-
def transform_stylesheet(stylesheet_uri, content)
|
449
|
-
content.gsub(%r{url\(([^\)]+)\)}) do |_|
|
450
|
-
result = save_remote_file(stylesheet_uri, :image, $1)
|
451
|
-
"url(#{result || $1})"
|
452
|
-
end
|
453
|
-
end
|
454
|
-
|
455
|
-
def save_remote_file(base_uri, type, url)
|
456
|
-
# FIXME: two different files could have the same basename :)
|
457
|
-
uri = URI.parse(url)
|
458
|
-
remote_uri = uri.absolute? ? uri : base_uri.merge(uri)
|
459
|
-
basename = File.basename(uri.path)
|
460
|
-
|
461
|
-
unless @already_collected.include?(remote_uri)
|
118
|
+
def save_remote_file(base_uri, type, url)
|
119
|
+
# FIXME: two different files could have the same basename :)
|
462
120
|
begin
|
463
|
-
|
464
|
-
|
465
|
-
local_path = File.join(@source_output_dir, basename)
|
466
|
-
File.open(local_path, "w") { |f| f.write(content) }
|
467
|
-
@already_collected << remote_uri
|
468
|
-
rescue Errno::ENOENT, OpenURI::HTTPError, Net::FTPPermError
|
121
|
+
uri = URI.parse(url)
|
122
|
+
rescue URI::InvalidURIError
|
469
123
|
return nil
|
470
124
|
end
|
125
|
+
remote_uri = uri.absolute? ? uri : base_uri.merge(uri)
|
126
|
+
basename = File.basename(uri.path)
|
127
|
+
|
128
|
+
unless @already_collected.include?(remote_uri)
|
129
|
+
begin
|
130
|
+
content = open(remote_uri.to_s).read
|
131
|
+
content = transform_stylesheet(remote_uri, content) if type == :stylesheet
|
132
|
+
local_path = File.join(@assets_dir, basename)
|
133
|
+
File.open(local_path, "w") { |f| f.write(content) }
|
134
|
+
@already_collected << remote_uri
|
135
|
+
rescue IOError, Errno::ENOENT, OpenURI::HTTPError, Net::FTPPermError
|
136
|
+
return nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
basename
|
471
140
|
end
|
472
|
-
basename
|
473
|
-
end
|
474
141
|
end
|
475
142
|
end
|
data/lib/butternut.rb
CHANGED
@@ -6,20 +6,15 @@ module Butternut
|
|
6
6
|
def self.setup_hooks(obj)
|
7
7
|
obj.instance_exec do
|
8
8
|
AfterStep do |object|
|
9
|
-
|
10
|
-
if
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
object.last_page_url = nil
|
17
|
-
end
|
18
|
-
@page_changed = false
|
9
|
+
if object.is_a?(Cucumber::Ast::Scenario)
|
10
|
+
if page_changed?
|
11
|
+
object.last_page_source = current_page_source
|
12
|
+
object.last_page_url = current_url
|
13
|
+
else
|
14
|
+
object.last_page_source = nil
|
15
|
+
object.last_page_url = nil
|
19
16
|
end
|
20
|
-
|
21
|
-
p e
|
22
|
-
pp caller
|
17
|
+
@page_changed = false
|
23
18
|
end
|
24
19
|
end
|
25
20
|
end
|
@@ -5,19 +5,6 @@ module Butternut
|
|
5
5
|
extend SpecHelperDsl
|
6
6
|
include SpecHelper
|
7
7
|
|
8
|
-
Spec::Matchers.define :have_css_node do |css, regexp|
|
9
|
-
match do |doc|
|
10
|
-
nodes = doc.css(css)
|
11
|
-
nodes.detect{ |node| node.text =~ regexp }
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
Spec::Matchers.define :be_an_existing_file do
|
16
|
-
match do |filename|
|
17
|
-
File.exist?(filename)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
8
|
def setup_formatter(options = {})
|
22
9
|
@out = StringIO.new
|
23
10
|
@formatter = Butternut::Formatter.new(step_mother, @out, options)
|
@@ -33,191 +20,11 @@ module Butternut
|
|
33
20
|
files.detect { |f| f.to_s =~ /\.html$/ }
|
34
21
|
end
|
35
22
|
|
36
|
-
|
37
|
-
|
38
|
-
setup_formatter
|
39
|
-
end
|
40
|
-
|
41
|
-
it "should not raise an error when visiting a blank feature name" do
|
42
|
-
lambda { @formatter.feature_name("") }.should_not raise_error
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
describe "given a single feature" do
|
47
|
-
before(:each) do
|
48
|
-
setup_formatter
|
49
|
-
run_defined_feature
|
50
|
-
@doc = Nokogiri.HTML(@out.string)
|
51
|
-
end
|
52
|
-
|
53
|
-
describe "with a comment" do
|
54
|
-
define_feature <<-FEATURE
|
55
|
-
# Healthy
|
56
|
-
FEATURE
|
57
|
-
|
58
|
-
it { @out.string.should =~ /^\<!DOCTYPE/ }
|
59
|
-
it { @out.string.should =~ /\<\/html\>$/ }
|
60
|
-
it { @doc.should have_css_node('.feature .comment', /Healthy/) }
|
61
|
-
end
|
62
|
-
|
63
|
-
describe "with a tag" do
|
64
|
-
define_feature <<-FEATURE
|
65
|
-
@foo
|
66
|
-
FEATURE
|
67
|
-
|
68
|
-
it { @doc.should have_css_node('.feature .tag', /foo/) }
|
69
|
-
end
|
70
|
-
|
71
|
-
describe "with a narrative" do
|
72
|
-
define_feature <<-FEATURE
|
73
|
-
Feature: Bananas
|
74
|
-
In order to find my inner monkey
|
75
|
-
As a human
|
76
|
-
I must eat bananas
|
77
|
-
FEATURE
|
78
|
-
|
79
|
-
it { @doc.should have_css_node('.feature h2', /Bananas/) }
|
80
|
-
it { @doc.should have_css_node('.feature .narrative', /must eat bananas/) }
|
81
|
-
end
|
82
|
-
|
83
|
-
describe "with a background" do
|
84
|
-
define_feature <<-FEATURE
|
85
|
-
Feature: Bananas
|
86
|
-
|
87
|
-
Background:
|
88
|
-
Given there are bananas
|
89
|
-
FEATURE
|
90
|
-
|
91
|
-
it { @doc.should have_css_node('.feature .background', /there are bananas/) }
|
92
|
-
end
|
93
|
-
|
94
|
-
describe "with a scenario" do
|
95
|
-
define_feature <<-FEATURE
|
96
|
-
Scenario: Monkey eats banana
|
97
|
-
Given there are bananas
|
98
|
-
FEATURE
|
99
|
-
|
100
|
-
it { @doc.should have_css_node('.feature h3', /Monkey eats banana/) }
|
101
|
-
it { @doc.should have_css_node('.feature .scenario .step', /there are bananas/) }
|
102
|
-
end
|
103
|
-
|
104
|
-
describe "with a scenario outline" do
|
105
|
-
define_feature <<-FEATURE
|
106
|
-
Scenario Outline: Monkey eats a balanced diet
|
107
|
-
Given there are <Things>
|
108
|
-
|
109
|
-
Examples: Fruit
|
110
|
-
| Things |
|
111
|
-
| apples |
|
112
|
-
| bananas |
|
113
|
-
Examples: Vegetables
|
114
|
-
| Things |
|
115
|
-
| broccoli |
|
116
|
-
| carrots |
|
117
|
-
FEATURE
|
118
|
-
|
119
|
-
it { @doc.should have_css_node('.feature .scenario.outline h4', /Fruit/) }
|
120
|
-
it { @doc.should have_css_node('.feature .scenario.outline h4', /Vegetables/) }
|
121
|
-
it { @doc.css('.feature .scenario.outline h4').length.should == 2}
|
122
|
-
it { @doc.should have_css_node('.feature .scenario.outline table', //) }
|
123
|
-
it { @doc.should have_css_node('.feature .scenario.outline table td', /carrots/) }
|
124
|
-
end
|
125
|
-
|
126
|
-
describe "with a step with a py string" do
|
127
|
-
define_feature <<-FEATURE
|
128
|
-
Scenario: Monkey goes to town
|
129
|
-
Given there is a monkey called:
|
130
|
-
"""
|
131
|
-
foo
|
132
|
-
"""
|
133
|
-
FEATURE
|
134
|
-
|
135
|
-
it { @doc.should have_css_node('.feature .scenario .val', /foo/) }
|
136
|
-
end
|
137
|
-
|
138
|
-
describe "with a multiline step arg" do
|
139
|
-
define_feature <<-FEATURE
|
140
|
-
Scenario: Monkey goes to town
|
141
|
-
Given there are monkeys:
|
142
|
-
| name |
|
143
|
-
| foo |
|
144
|
-
| bar |
|
145
|
-
FEATURE
|
146
|
-
|
147
|
-
it { @doc.should have_css_node('.feature .scenario table td', /foo/) }
|
148
|
-
end
|
149
|
-
|
150
|
-
describe "with a table in the background and the scenario" do
|
151
|
-
define_feature <<-FEATURE
|
152
|
-
Background:
|
153
|
-
Given table:
|
154
|
-
| a | b |
|
155
|
-
| c | d |
|
156
|
-
Scenario:
|
157
|
-
Given another table:
|
158
|
-
| e | f |
|
159
|
-
| g | h |
|
160
|
-
FEATURE
|
161
|
-
|
162
|
-
it { @doc.css('td').length.should == 8 }
|
163
|
-
end
|
164
|
-
|
165
|
-
describe "with a py string in the background and the scenario" do
|
166
|
-
define_feature <<-FEATURE
|
167
|
-
Background:
|
168
|
-
Given stuff:
|
169
|
-
"""
|
170
|
-
foo
|
171
|
-
"""
|
172
|
-
Scenario:
|
173
|
-
Given more stuff:
|
174
|
-
"""
|
175
|
-
bar
|
176
|
-
"""
|
177
|
-
FEATURE
|
178
|
-
|
179
|
-
it { @doc.css('.feature .background pre.val').length.should == 1 }
|
180
|
-
it { @doc.css('.feature .scenario pre.val').length.should == 1 }
|
181
|
-
end
|
182
|
-
|
183
|
-
describe "with a step that fails in the scenario" do
|
184
|
-
define_steps do
|
185
|
-
Given(/boo/) { raise 'eek' }
|
186
|
-
end
|
187
|
-
|
188
|
-
define_feature(<<-FEATURE)
|
189
|
-
Scenario: Monkey gets a fright
|
190
|
-
Given boo
|
191
|
-
FEATURE
|
192
|
-
|
193
|
-
it { @doc.should have_css_node('.feature .scenario .step.failed', /eek/) }
|
194
|
-
end
|
195
|
-
|
196
|
-
describe "with a step that fails in the backgound" do
|
197
|
-
define_steps do
|
198
|
-
Given(/boo/) { raise 'eek' }
|
199
|
-
end
|
200
|
-
|
201
|
-
define_feature(<<-FEATURE)
|
202
|
-
Background:
|
203
|
-
Given boo
|
204
|
-
Scenario:
|
205
|
-
Given yay
|
206
|
-
FEATURE
|
207
|
-
|
208
|
-
it { @doc.should have_css_node('.feature .background .step.failed', /eek/) }
|
209
|
-
it { @doc.should_not have_css_node('.feature .scenario .step.failed', //) }
|
210
|
-
it { @doc.should have_css_node('.feature .scenario .step.undefined', /yay/) }
|
211
|
-
end
|
23
|
+
it "should be a subclass of the html formatter" do
|
24
|
+
Butternut::Formatter.superclass.should == Cucumber::Formatter::Html
|
212
25
|
end
|
213
26
|
|
214
|
-
describe "
|
215
|
-
before(:each) do
|
216
|
-
setup_formatter
|
217
|
-
run_defined_feature
|
218
|
-
@doc = Nokogiri.HTML(@out.string)
|
219
|
-
end
|
220
|
-
|
27
|
+
describe "running without the --out option" do
|
221
28
|
define_steps do
|
222
29
|
Given(/foo/) do
|
223
30
|
visit("file://" + File.expand_path(File.dirname(__FILE__) + "/../fixtures/foo.html"))
|
@@ -229,89 +36,110 @@ module Butternut
|
|
229
36
|
Given foo
|
230
37
|
FEATURE
|
231
38
|
|
232
|
-
it do
|
233
|
-
|
234
|
-
|
235
|
-
|
39
|
+
it "should raise an error" do
|
40
|
+
lambda {
|
41
|
+
setup_formatter
|
42
|
+
run_defined_feature
|
43
|
+
}.should raise_error
|
236
44
|
end
|
237
45
|
end
|
238
46
|
|
239
|
-
describe "
|
47
|
+
describe "running with the --out option" do
|
240
48
|
before(:each) do
|
241
49
|
dir = File.join(File.dirname(__FILE__), "..", "..", "tmp")
|
242
|
-
|
243
|
-
|
244
|
-
]})
|
245
|
-
run_defined_feature
|
246
|
-
@doc = Nokogiri.HTML(@out.string)
|
50
|
+
@tmp_dir = File.join(dir, "#{Time.now.to_i}-#{rand(1000)}")
|
51
|
+
FileUtils.mkdir(@tmp_dir)
|
247
52
|
|
248
|
-
|
249
|
-
file = most_recent_html_file(@tmp_dir)
|
250
|
-
|
53
|
+
#@tmp_dir = File.join(dir, "features", Date.today.to_s)
|
54
|
+
#file = most_recent_html_file(@tmp_dir)
|
55
|
+
#@page_doc = Nokogiri.HTML(open(file).read)
|
251
56
|
end
|
252
57
|
|
253
|
-
|
254
|
-
|
255
|
-
|
58
|
+
describe "with a filename specified" do
|
59
|
+
define_steps do
|
60
|
+
Given(/foo/) do
|
61
|
+
visit("file://" + File.join(FIXTURE_DIR, "foo.html"))
|
62
|
+
end
|
256
63
|
end
|
257
|
-
end
|
258
64
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
65
|
+
define_feature(<<-FEATURE)
|
66
|
+
Scenario: Monkey goes to the zoo
|
67
|
+
Given foo
|
68
|
+
Then bar
|
69
|
+
FEATURE
|
263
70
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
71
|
+
before(:each) do
|
72
|
+
setup_formatter({
|
73
|
+
:formats => [
|
74
|
+
['Butternut::Formatter', File.join(@tmp_dir, "output.html")]
|
75
|
+
]
|
76
|
+
})
|
77
|
+
run_defined_feature
|
78
|
+
@doc = Nokogiri.HTML(@out.string)
|
79
|
+
|
80
|
+
file = most_recent_html_file(File.join(@tmp_dir, "output"))
|
81
|
+
@page_doc = Nokogiri.HTML(open(file).read)
|
82
|
+
end
|
271
83
|
|
272
|
-
|
273
|
-
|
274
|
-
|
84
|
+
it "creates assets directory" do
|
85
|
+
File.join(@tmp_dir, "output").should be_an_existing_directory
|
86
|
+
end
|
275
87
|
|
276
|
-
|
277
|
-
|
88
|
+
it "links to the page source" do
|
89
|
+
step = @doc.at('.feature .scenario .step.passed')
|
90
|
+
link = step.at("a")
|
91
|
+
link.should_not be_nil
|
92
|
+
file = link['href']
|
93
|
+
file.should match(%r{^output/butternut.+\.html})
|
94
|
+
end
|
278
95
|
|
279
|
-
|
280
|
-
|
281
|
-
|
96
|
+
it "saves images and stylesheets and rewrites urls in page source" do
|
97
|
+
@page_doc.at('img:nth(1)')['src'].should == "picard.jpg"
|
98
|
+
File.join(@tmp_dir, "output", "picard.jpg").should be_an_existing_file
|
282
99
|
|
283
|
-
|
284
|
-
|
285
|
-
foo.should include("url(facepalm.jpg)")
|
286
|
-
File.join(@tmp_dir, "facepalm.jpg").should be_an_existing_file
|
287
|
-
end
|
100
|
+
@page_doc.at('link:nth(1)[rel="stylesheet"]')['href'].should == "foo.css"
|
101
|
+
File.join(@tmp_dir, "output", "foo.css").should be_an_existing_file
|
288
102
|
|
289
|
-
|
290
|
-
|
291
|
-
link['href'].should == "#"
|
103
|
+
@page_doc.at('link:nth(2)[rel="stylesheet"]')['href'].should == "bar.css"
|
104
|
+
File.join(@tmp_dir, "output", "bar.css").should be_an_existing_file
|
292
105
|
end
|
293
|
-
end
|
294
106
|
|
295
|
-
|
296
|
-
|
297
|
-
|
107
|
+
it "saves assets and rewrites urls referred to by stylesheets" do
|
108
|
+
foo = open(File.join(@tmp_dir, "output", "foo.css")).read
|
109
|
+
foo.should include("url(facepalm.jpg)")
|
110
|
+
File.join(@tmp_dir, "output", "facepalm.jpg").should be_an_existing_file
|
111
|
+
end
|
298
112
|
|
299
|
-
|
300
|
-
|
301
|
-
|
113
|
+
it "turns off links" do
|
114
|
+
@page_doc.css('a').each do |link|
|
115
|
+
link['href'].should == "#"
|
116
|
+
end
|
302
117
|
end
|
303
|
-
end
|
304
118
|
|
305
|
-
|
306
|
-
|
307
|
-
|
119
|
+
it "turns off scripts" do
|
120
|
+
@page_doc.css('script').length.should == 0
|
121
|
+
end
|
308
122
|
|
309
|
-
|
310
|
-
|
311
|
-
|
123
|
+
it "disables form elements" do
|
124
|
+
@page_doc.css('input, select, textarea').each do |elt|
|
125
|
+
elt['disabled'].should == "disabled"
|
126
|
+
end
|
127
|
+
end
|
312
128
|
|
313
|
-
|
314
|
-
|
129
|
+
it "handles Errno::ENOENT" do
|
130
|
+
@page_doc.at('img:nth(2)')['src'].should == "/roflpwnage/missing_file_omg.gif"
|
131
|
+
end
|
132
|
+
|
133
|
+
it "handles OpenURI::HTTPError" do
|
134
|
+
@page_doc.at('img:nth(3)')['src'].should == "http://google.com/missing_file_omg.gif"
|
135
|
+
end
|
136
|
+
|
137
|
+
it "handles Net::FTPPermError" do
|
138
|
+
@page_doc.at('img:nth(4)')['src'].should == "ftp://mirror.anl.gov/missing_file_omg.gif"
|
139
|
+
end
|
140
|
+
|
141
|
+
it "handles badly formed URI's" do
|
142
|
+
end
|
315
143
|
end
|
316
144
|
end
|
317
145
|
end
|
data/spec/fixtures/foo.html
CHANGED
data/spec/spec.opts
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -2,7 +2,7 @@ require 'rubygems'
|
|
2
2
|
gem 'rspec'
|
3
3
|
require 'spec'
|
4
4
|
require 'spec/autorun'
|
5
|
-
require '
|
5
|
+
require 'fileutils'
|
6
6
|
|
7
7
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
8
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
@@ -71,6 +71,25 @@ module SpecHelper
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
+
FIXTURE_DIR = File.expand_path(File.join(File.dirname(__FILE__), "fixtures"))
|
75
|
+
|
76
|
+
Spec::Matchers.define :be_an_existing_file do
|
77
|
+
match { |filename| File.exist?(filename) }
|
78
|
+
end
|
79
|
+
|
80
|
+
Spec::Matchers.define :be_an_existing_directory do
|
81
|
+
match { |filename| File.directory?(filename) }
|
82
|
+
end
|
83
|
+
|
84
|
+
Spec::Matchers.define :match_content_of do |expected|
|
85
|
+
match do |actual|
|
86
|
+
raise "expected file doesn't exist" unless File.exist?(expected)
|
87
|
+
raise "actual file doesn't exist" unless File.exist?(actual)
|
88
|
+
open(expected).read == open(actual).read
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
74
93
|
Spec::Runner.configure do |config|
|
75
94
|
config.before(:each) do
|
76
95
|
Cucumber::Parser::NaturalLanguage.instance_variable_set(:@languages, nil)
|
File without changes
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: butternut
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Stephens
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-12-14 00:00:00 Z
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -86,8 +86,7 @@ files:
|
|
86
86
|
- spec/fixtures/picard.jpg
|
87
87
|
- spec/spec.opts
|
88
88
|
- spec/spec_helper.rb
|
89
|
-
- tmp
|
90
|
-
- tmp/main/.gitignore
|
89
|
+
- tmp/.gitignore
|
91
90
|
has_rdoc: true
|
92
91
|
homepage: http://github.com/viking/butternut
|
93
92
|
licenses: []
|
data/tmp/main/.gitignore
DELETED