nitrous 1.0.3

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/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "nitrous"
5
+ gemspec.summary = "A half-baked integration testing framework"
6
+ gemspec.description = ""
7
+ gemspec.email = "austin.taylor@gmail.com"
8
+ gemspec.homepage = "http://github.com/austintaylor/nitrous"
9
+ gemspec.authors = ["Austin Taylor", "Paul Nicholson"]
10
+ end
11
+ rescue LoadError
12
+ puts "Jeweler not available. Install it with: gem install jeweler"
13
+ end
14
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.3
data/cmd_test ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ require 'command_line'
3
+
4
+ class CmdTest < CommandLineUtility
5
+ describe :magic, "does some magical stuff"
6
+ def magic
7
+ puts "magic"
8
+ end
9
+ end
10
+
11
+ CmdTest.run
data/command_line.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'activesupport'
3
+ class CommandLineUtility
4
+ class << self
5
+ def inherited(klass)
6
+ klass.descriptions = descriptions.dup
7
+ end
8
+
9
+ def run
10
+ return unless name.underscore == $0.split("/").last
11
+ instance = self.new
12
+ if ARGV.empty?
13
+ instance.default
14
+ else
15
+ instance.send(ARGV.first, *ARGV[1..-1])
16
+ end
17
+ end
18
+
19
+ def descriptions=(descriptions)
20
+ @descriptions = descriptions
21
+ end
22
+
23
+ def descriptions
24
+ @descriptions ||= HashWithIndifferentAccess.new
25
+ end
26
+
27
+ def describe(command, description)
28
+ descriptions[command] = description
29
+ end
30
+
31
+ def description_for(command)
32
+ descriptions[command]
33
+ end
34
+
35
+ def commands
36
+ self.public_instance_methods.sort - Object.public_instance_methods - ["default"]
37
+ end
38
+ end
39
+
40
+ describe :help, "Print this help"
41
+ def help
42
+ puts "usage: #{self.class.name.underscore} [COMMAND [ARGS]]"
43
+ puts ""
44
+ puts "Commands:"
45
+ stuff = self.class.commands.map do |command|
46
+ description = self.class.description_for(command)
47
+ description ? " #{command} (#{description})" : " #{command}"
48
+ end
49
+ puts stuff
50
+ end
51
+
52
+ alias_method :default, :help
53
+ end
data/example.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rails_env'
2
+ p $test
3
+ $test = true
4
+ puts Organization.count
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'nitrous/server'
data/lib/core_ext.rb ADDED
@@ -0,0 +1,27 @@
1
+ if !Array.instance_methods.include?('sum')
2
+ class Array
3
+ def sum
4
+ inject(0) do |sum, each|
5
+ block_given? ? sum + yield(each) : sum + each
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ class Symbol
12
+ def to_proc
13
+ Proc.new { |*args| args.shift.__send__(self, *args) }
14
+ end
15
+ end
16
+
17
+ class Exception
18
+ def test_output
19
+ to_s + "\n" + backtrace.join("\n")
20
+ end
21
+ end
22
+
23
+ class Hash
24
+ def to_s
25
+ map {|k,v| "#{k}: #{v}"}.join("\r\n")
26
+ end
27
+ end
data/lib/nitrous.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "nitrous/test"
2
+
3
+ module Nitrous
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,95 @@
1
+ module Nitrous
2
+ class AssertionFailedError < Exception
3
+ def initialize(message, filename)
4
+ @message, @filename = message, filename
5
+ end
6
+
7
+ def failure_location
8
+ @failure_location ||= backtrace.detect do |line|
9
+ line.include?(@filename)
10
+ end
11
+ end
12
+
13
+ def snippet
14
+ failure_location =~ /^([^:]+):(\d+)/
15
+ index = $2.to_i - 1
16
+ lines = File.readlines($1)
17
+ "...\n" +
18
+ " " + lines[index - 1] +
19
+ " >>" + lines[index] +
20
+ " " + lines[index + 1] +
21
+ "...\n"
22
+ end
23
+
24
+ def test_output
25
+ "Assertion failed on #{failure_location}\n#{@message}\n#{snippet}"
26
+ end
27
+ end
28
+
29
+ module Assertions
30
+ def self.method_added(method)
31
+ return unless method.to_s =~ /!$/
32
+ name = method.to_s.gsub("!", '')
33
+ module_eval <<-"end;"
34
+ def #{name}(*args, &b)
35
+ collect_errors do
36
+ #{method}(*args, &b)
37
+ end
38
+ end
39
+ end;
40
+ end
41
+
42
+ def self.included(mod)
43
+ mod.module_eval do
44
+ def self.method_added(method)
45
+ Assertions.method_added(method)
46
+ end
47
+ end
48
+ end
49
+
50
+ def fail(message)
51
+ raise AssertionFailedError.new(message, @current_test.filename)
52
+ end
53
+
54
+ def assert!(value, message=nil)
55
+ fail(message || "#{value.inspect} is not true.") unless value
56
+ yield if block_given?
57
+ end
58
+
59
+ def assert_nil!(value)
60
+ fail("#{value.inspect} is not nil.") unless value.nil?
61
+ yield if block_given?
62
+ end
63
+
64
+ def assert_equal!(expected, actual, message=nil)
65
+ fail(message || "Expected: <#{expected}> but was <#{actual}>") unless expected == actual
66
+ yield if block_given?
67
+ end
68
+
69
+ def assert_not_equal!(not_expected, actual, message=nil)
70
+ fail(message || "Expected: <#{not_expected}> not to equal <#{actual}>") unless not_expected != actual
71
+ yield if block_given?
72
+ end
73
+
74
+ def assert_match!(pattern, string, message=nil)
75
+ pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
76
+ fail(message || "#{string} expected to be =~ #{pattern}") unless string =~ pattern
77
+ end
78
+
79
+ def assert_raise!(type=Exception, &block)
80
+ yield
81
+ passed = true
82
+ raise
83
+ rescue Exception => e
84
+ fail("Expected a(n) #{type} to be raised but raised a(n) #{e.class}") if e.class != type
85
+ fail("Expected a(n) #{type} to be raised") if passed
86
+ end
87
+
88
+ def assert_not_raised!(type=Exception, &block)
89
+ yield
90
+ rescue type => e
91
+ fail("Expected a(n) #{type} not to be raised but a(n) #{e.class} was raised.\n#{e.message}")
92
+ end
93
+ alias_method :assert_nothing_raised!, :assert_not_raised!
94
+ end
95
+ end
@@ -0,0 +1,35 @@
1
+ require 'webrick'
2
+ require 'fileutils'
3
+ require File.join(File.dirname(__FILE__), 'http_io')
4
+ FileUtils.cd(ARGV[0])
5
+ ENV["RAILS_ENV"] = "test"
6
+ require "config/environment"
7
+
8
+ module RailsEnv
9
+ def self.exit_blocks
10
+ @exit_blocks ||= []
11
+ end
12
+ end
13
+ module Kernel
14
+ def at_exit(&block)
15
+ RailsEnv.exit_blocks << block
16
+ end
17
+ end
18
+
19
+ server = WEBrick::HTTPServer.new(:Port => 4034)
20
+ server.mount_proc("/run_test") do |req, res|
21
+ # if pid = fork
22
+ # Process.wait(pid)
23
+ # else
24
+ # $stdout = Nitrous::HttpIO.new
25
+ # res.body = $stdout
26
+ io = Nitrous::HttpIO.new
27
+ res.body = io
28
+ io.write 'hello'
29
+ # load req.query['file']
30
+ # RailsEnv.exit_blocks.each(&:call)
31
+ io.close
32
+ # $stdout.close
33
+ # end
34
+ end
35
+ server.start
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'daemons'
3
+
4
+ RAILS_ROOT = ARGV.last
5
+ options = {
6
+ :ARGV => [RAILS_ROOT]
7
+ :app_name => 'nitrous_server',
8
+ :dir_mode => :normal,
9
+ :dir => File.join(RAILS_ROOT, 'tmp/pids/')
10
+ }
11
+ Daemons.run(File.join(File.dirname(__FILE__), 'daemon.rb'), options)
@@ -0,0 +1,34 @@
1
+ require 'stringio'
2
+
3
+ module Nitrous
4
+ class HttpIO
5
+
6
+ def initialize
7
+ @string = String.new
8
+ end
9
+
10
+ def is_a?(klass)
11
+ klass == IO ? true : super(klass);
12
+ end
13
+
14
+ def size
15
+ 0
16
+ end
17
+
18
+ def read(len=nil)
19
+ sleep 1 while @string.empty? && !@closed
20
+ return nil if @closed
21
+ string = @string.dup
22
+ @string = ""
23
+ string
24
+ end
25
+
26
+ def write(string)
27
+ @string << string
28
+ end
29
+
30
+ def close
31
+ @closed = true
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,372 @@
1
+ require 'action_controller/assertions/selector_assertions'
2
+ require 'rails_ext'
3
+ require 'mime/types'
4
+ require 'active_support'
5
+ require 'action_controller'
6
+ require 'fileutils'
7
+ require 'patron'
8
+ module Nitrous
9
+ class IntegrationTest < RailsTest
10
+ SERVER_PORT = 4022
11
+ include ActionController::Assertions::SelectorAssertions
12
+ attr_accessor :cookies, :response, :status, :headers, :current_uri
13
+ at_exit {start_server}
14
+
15
+ def self.start_server
16
+ @server_thread = Thread.start do
17
+ options = {
18
+ :Port => SERVER_PORT,
19
+ :Host => "0.0.0.0",
20
+ :environment => (ENV['RAILS_ENV'] || "development").dup,
21
+ :config => RAILS_ROOT + "/config.ru",
22
+ :detach => false,
23
+ :debugger => false,
24
+ :path => nil
25
+ }
26
+
27
+ server = Rack::Handler::WEBrick
28
+ # begin
29
+ # server = Rack::Handler::Mongrel
30
+ # rescue LoadError => e
31
+ # end
32
+
33
+ ENV["RAILS_ENV"] = options[:environment]
34
+ RAILS_ENV.replace(options[:environment]) if defined?(RAILS_ENV)
35
+
36
+ if File.exist?(options[:config])
37
+ config = options[:config]
38
+ if config =~ /\.ru$/
39
+ cfgfile = File.read(config)
40
+ if cfgfile[/^#\\(.*)/]
41
+ opts.parse!($1.split(/\s+/))
42
+ end
43
+ inner_app = eval("Rack::Builder.new {( " + cfgfile + "\n )}.to_app", nil, config)
44
+ else
45
+ require config
46
+ inner_app = Object.const_get(File.basename(config, '.rb').capitalize)
47
+ end
48
+ else
49
+ require RAILS_ROOT + "/config/environment"
50
+ inner_app = ActionController::Dispatcher.new
51
+ end
52
+
53
+ app = Rack::Builder.new {
54
+ # use Rails::Rack::LogTailer unless options[:detach]
55
+ use Rails::Rack::Debugger if options[:debugger]
56
+ map '/' do
57
+ use Rails::Rack::Static
58
+ run inner_app
59
+ end
60
+ }.to_app
61
+
62
+ trap(:INT) { exit }
63
+
64
+ server.run(app, options.merge(:AccessLog => [], :Logger => WEBrick::Log.new("/dev/null")))
65
+
66
+ # Socket.do_not_reverse_lookup = true # patch for OS X
67
+ # server = WEBrick::HTTPServer.new(:BindAddress => '0.0.0.0', :ServerType => WEBrick::SimpleServer, :Port => 4022, :AccessLog => [], :Logger => WEBrick::Log.new("/dev/null"))
68
+ # server.mount('/', DispatchServlet, :server_root => File.expand_path(RAILS_ROOT + "/public/"))
69
+ # Rack::Handler::Mongrel.start(app, :Host => '0.0.0.0', :Port => 4022, :config => RAILS_ROOT + "/config.ru", :AccessLog => [])
70
+ end
71
+ sleep 0.001 until @server_thread.status == "sleep"
72
+ end
73
+
74
+ ActionController::Routing::Routes.install_helpers(self)
75
+ def url_for(options)
76
+ if options.delete(:only_path)
77
+ ActionController::Routing::Routes.generate(options)
78
+ else
79
+ "http://localhost:#{SERVER_PORT}" + ActionController::Routing::Routes.generate(options)
80
+ end
81
+ end
82
+
83
+ def navigate_to(path, headers={})
84
+ get path, nil, headers
85
+ follow_redirect! if redirect?
86
+ puts response.body if error?
87
+ assert !error?
88
+ end
89
+
90
+ BOUNDARY = 'multipart-boundary000'
91
+ def submit_form(id, data = {})
92
+ @redisplay = false
93
+ id, data = nil, id if id.is_a?(Hash)
94
+ form = css_select(id ? "form##{id}" : "form").first
95
+ fail(id ? "Form not found with id <#{id}>" : "No form found") unless form
96
+ validate = data.delete(:validate)
97
+ validate_form_fields(form, data) unless validate == false
98
+ fields = data.to_fields.reverse_merge(existing_values(form))
99
+ if form['enctype'] == 'multipart/form-data'
100
+ self.send(form["method"], form["action"], multipart_encode(fields), {'Content-Type' => "multipart/form-data, boundary=#{BOUNDARY}"})
101
+ else
102
+ self.send(form["method"], form["action"], fields)
103
+ end
104
+ puts response.body if error?
105
+ assert !error?
106
+ @redisplay = true if !redirect? && (id ? css_select("form##{id}").first : true)
107
+ follow_redirect! if redirect?
108
+ puts response.body if error?
109
+ assert !error?
110
+ end
111
+
112
+ def post_form(url, data={}, method = :post)
113
+ fields = data.to_fields
114
+ if fields.values.any? {|v| v.respond_to?(:read)}
115
+ self.send(method, url, multipart_encode(fields), {'Content-Type' => "multipart/form-data, boundary=#{BOUNDARY}"})
116
+ else
117
+ self.send(method, url, fields)
118
+ end
119
+ end
120
+
121
+ def multipart_encode(fields)
122
+ data = ""
123
+ fields.to_fields.each do |key, value|
124
+ data << "--#{BOUNDARY}\r\n"
125
+ if value.respond_to?(:read)
126
+ filename = File.basename(value.path)
127
+ data << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"\r\n"
128
+ data << "Content-Transfer-Encoding: binary\r\n"
129
+ data << "Content-Type: #{MIME::Types.type_for(filename)}\r\n\r\n"
130
+ data << value.read
131
+ else
132
+ data << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
133
+ data << value.to_s
134
+ end
135
+ data << "\r\n"
136
+ end
137
+ data << "--#{BOUNDARY}--"
138
+ data
139
+ end
140
+
141
+ def click_link(url, method=:get)
142
+ if method == :delete
143
+ elements = css_select("*[href=#{url}][onclick]")
144
+ fail("No link found with url <#{url}> and method delete") if elements.empty? || !elements.any?{|element| element["onclick"] =~ /m.setAttribute\('name', '_method'\);.*?m.setAttribute\('value', 'delete'\);/}
145
+ delete url
146
+ follow_redirect! if redirect?
147
+ puts response.body if error?
148
+ assert !error?
149
+ else
150
+ fail("No link found with url <#{url}>") unless css_select("*[href=#{url}]").first
151
+ navigate_to(url)
152
+ end
153
+ end
154
+
155
+ def assert_form_redisplayed!
156
+ fail("Expected form to redisplay. Redirected to <#{current_uri}>") unless @redisplay
157
+ end
158
+
159
+ def field_value(name)
160
+ css_select(html_document.root, "input, select, textarea").detect {|field| field["name"] == name}["value"]
161
+ end
162
+
163
+ def assert_viewing(request_uri, message=nil)
164
+ fail("Expected page but recieved redirect. <#{current_uri}>") unless success?
165
+ assert_match %r(#{Regexp.escape(request_uri)}(\?|&|$)), current_uri, message
166
+ end
167
+
168
+ def assert_page_contains!(string)
169
+ fail("Expected page to contain <#{string}> but it did not. Page:\n#{response.body}") unless response.body.include?(string.to_s)
170
+ end
171
+
172
+ def assert_not_page_contains!(string)
173
+ fail("Expected page not to contain <#{string}> but it did. Page:\n#{response.body}") if response.body.include?(string.to_s)
174
+ end
175
+
176
+ def assert_form_values!(id, data={})
177
+ id, data = nil, id if id.is_a?(Hash)
178
+ form = css_select(id ? "form##{id}" : "form").first
179
+ fail(id ? "Form not found with id <#{id}>" : "No form found") unless form
180
+ data.to_fields.each do |name, value|
181
+ form_fields = css_select form, "input, select, textarea"
182
+ matching_fields = form_fields.select {|field| (field["name"] == name || field["name"] == "#{name}[]") && (!%w(radio checkbox).include?(field['type']) || field['checked'] == 'checked')}
183
+
184
+ # Handle boolean checkboxes
185
+ matching_field = matching_fields.detect {|f| f['checked'] == 'checked'} || matching_fields.first
186
+
187
+ fail "Could not find a form field having the name #{name}" unless matching_field
188
+ case matching_field.name.downcase
189
+ when 'input'
190
+ fail "Expected value of field #{name} to be #{value} but was #{matching_field['value']}" unless value.to_s == matching_field['value']
191
+ when 'textarea'
192
+ assert_equal value.to_s, matching_field.children.first.to_s
193
+ when 'select'
194
+ selected_option = css_select(matching_field, 'option[selected]').first
195
+ fail("No option selected for #{name}. Expected #{value} to be selected.") unless selected_option
196
+ assert_equal value.to_s, selected_option['value']
197
+ end
198
+ end
199
+ end
200
+
201
+ def existing_values(form)
202
+ inputs = css_select(form, 'input').reject {|i| %w(checkbox radio).include?(i['type']) && (i['checked'].blank? || i['checked'].downcase != 'checked')}
203
+ values = {}
204
+ inputs.each do |input|
205
+ values[input['name']] = input['value']
206
+ end
207
+ css_select(form, 'textarea').each do |textarea|
208
+ values[textarea['name']] = textarea.children.map(&:to_s).join
209
+ end
210
+ css_select(form, 'select').each do |select|
211
+ selected = css_select(select, 'option[selected]').first || css_select(select, 'option').first
212
+ values[select['name']] = selected['value'] if selected
213
+ end
214
+ values.each{|k, v| values[k] = '' if v.nil?}
215
+ end
216
+
217
+ def validate_form_fields(form, data)
218
+ data.to_fields.each do |name, value|
219
+ form_fields = css_select form, "input, select, textarea"
220
+ matching_field = form_fields.detect {|field| field["name"] == name || field["name"] == "#{name}[]"}
221
+ fail "Could not find a form field having the name #{name}" unless matching_field
222
+ assert_equal 'file', matching_field['type'] if value.is_a?(File)
223
+ assert_equal "multipart/form-data", form["enctype"], "Form <#{form['id']}> has a file field <#{name}>, but the enctype is not multipart/form-data" if matching_field["type"] == "file"
224
+ end
225
+ end
226
+
227
+ def view(email)
228
+ @response = ActionController::TestResponse.new
229
+ @response.body = email.body
230
+ @html_document = nil
231
+ end
232
+
233
+ def follow_redirect!
234
+ raise "not a redirect! #{@status} #{@status_message}" unless redirect?
235
+
236
+ location = URI.parse(headers['location'].first)
237
+ path = location.query ? "#{location.path}?#{location.query}" : location.path
238
+ domains = location.host.split('.')
239
+ subdomain = domains.length > 2 ? domains.first : nil
240
+ set_subdomain(subdomain) if subdomain != @subdomain
241
+
242
+ get(location.host.include?('localhost') ? path : headers['location'].first)
243
+ status
244
+ end
245
+
246
+ def html_document
247
+ xml = @response.content_type =~ /xml$/
248
+ @html_document ||= HTML::Document.new(@response.body, false, xml)
249
+ end
250
+
251
+ def get(path, parameters=nil, headers={})
252
+ headers['QUERY_STRING'] = requestify(parameters) || ""
253
+ process(headers, path) do
254
+ if(!headers['QUERY_STRING'].blank?)
255
+ http_session.get(path + "?#{headers['QUERY_STRING']}", headers)
256
+ else
257
+ http_session.get(path, headers)
258
+ end
259
+ end
260
+ end
261
+
262
+ def post(path, parameters=nil, headers={})
263
+ data = requestify(parameters) || ""
264
+ headers['CONTENT_LENGTH'] = data.length.to_s
265
+ process(headers, path) do
266
+ http_session.post(path, data, headers)
267
+ end
268
+ end
269
+
270
+ def delete(path, parameters=nil, headers={})
271
+ headers['QUERY_STRING'] = requestify(parameters) || ""
272
+ process(headers, path) do
273
+ http_session.delete(path, headers)
274
+ end
275
+ end
276
+
277
+ def put(path, parameters=nil, headers={})
278
+ data = requestify(parameters) || ""
279
+ headers['CONTENT_LENGTH'] = data.length.to_s
280
+ process(headers, path) do
281
+ http_session.put(path, data, headers)
282
+ end
283
+ end
284
+
285
+ def process(headers, path=nil)
286
+ headers['Cookie'] = encode_cookies unless encode_cookies.blank?
287
+ self.response = yield
288
+ self.current_uri = path
289
+ @html_document = nil
290
+ parse_result
291
+ end
292
+
293
+ # was the response successful?
294
+ def success?
295
+ status == 200
296
+ end
297
+
298
+ # was the URL not found?
299
+ def missing?
300
+ status == 404
301
+ end
302
+
303
+ # were we redirected?
304
+ def redirect?
305
+ (300..399).include?(status)
306
+ end
307
+
308
+ # was there a server-side error?
309
+ def error?
310
+ (500..599).include?(status)
311
+ end
312
+
313
+ private
314
+
315
+ def encode_cookies
316
+ (cookies||{}).inject("") do |string, (name, value)|
317
+ string << "#{name}=#{value}; "
318
+ end
319
+ end
320
+
321
+ def http_session
322
+ # @http ||= returning(Patron::Session.new) do |session|
323
+ # session.timeout = 10
324
+ # session.base_url = "http://localhost:#{SERVER_PORT}"
325
+ # session.headers['User-Agent'] = 'Nitrous/1.0'
326
+ # end
327
+ uri = URI.parse("http://localhost:#{SERVER_PORT}/") unless @http
328
+ @http ||= Net::HTTP.start(uri.host, uri.port)
329
+ end
330
+
331
+ def parse_result
332
+ @headers = @response.to_hash
333
+ @cookies ||= {}
334
+ (@headers['set-cookie'] || [] ).each do |string|
335
+ name, value = string.match(/^([^=]*)=([^;]*);/)[1,2]
336
+ @cookies[name] = value
337
+ end
338
+ @status, @status_message = @response.code.to_i, @response.message
339
+ end
340
+
341
+ def name_with_prefix(prefix, name)
342
+ prefix ? "#{prefix}[#{name}]" : name.to_s
343
+ end
344
+
345
+ def requestify(parameters, prefix=nil)
346
+ if Hash === parameters
347
+ return nil if parameters.empty?
348
+ parameters.map { |k,v| requestify(v, name_with_prefix(prefix, k)) }.join("&")
349
+ elsif Array === parameters
350
+ parameters.map { |v| requestify(v, name_with_prefix(prefix, "")) }.join("&")
351
+ elsif prefix.nil?
352
+ parameters
353
+ else
354
+ "#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}"
355
+ end
356
+ end
357
+
358
+ class DummyFile
359
+ def initialize(name, content)
360
+ @name, @content = name, content
361
+ end
362
+
363
+ def read
364
+ @content
365
+ end
366
+
367
+ def path
368
+ "/tmp/#{@name}"
369
+ end
370
+ end
371
+ end
372
+ end