nitrous 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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