fluffy 0.1.1

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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.gem
7
+ demo*
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Ben Wyrosdick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = fluffy
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Ben Wyrosdick. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "fluffy"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = "ben.wyrosdick@gmail.com"
10
+ gem.homepage = "http://github.com/commonthread/fluffy"
11
+ gem.authors = ["Ben Wyrosdick"]
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+ begin
40
+ require 'cucumber/rake/task'
41
+ Cucumber::Rake::Task.new(:features)
42
+ rescue LoadError
43
+ task :features do
44
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
45
+ end
46
+ end
47
+
48
+ task :default => :test
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ if File.exist?('VERSION.yml')
53
+ config = YAML.load(File.read('VERSION.yml'))
54
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
55
+ else
56
+ version = ""
57
+ end
58
+
59
+ rdoc.rdoc_dir = 'rdoc'
60
+ rdoc.title = "fluffy #{version}"
61
+ rdoc.rdoc_files.include('README*')
62
+ rdoc.rdoc_files.include('lib/**/*.rb')
63
+ end
64
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
File without changes
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'fluffy'
3
+
4
+ require 'test/unit/assertions'
5
+
6
+ World(Test::Unit::Assertions)
data/fluffy.gemspec ADDED
@@ -0,0 +1,51 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{fluffy}
5
+ s.version = "0.1.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Ben Wyrosdick"]
9
+ s.date = %q{2009-07-24}
10
+ s.email = %q{ben@commonthread.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".document",
17
+ ".gitignore",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "features/fluffy.feature",
23
+ "features/step_definitions/fluffy_steps.rb",
24
+ "features/support/env.rb",
25
+ "fluffy.gemspec",
26
+ "lib/fluffy.rb",
27
+ "lib/fluffy/s3_io.rb",
28
+ "lib/fluffy/s3_path.rb",
29
+ "test/fluffy_test.rb",
30
+ "test/test_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/commonthread/fluffy}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.4}
36
+ s.summary = %q{Makes writing to S3 tranparent using File}
37
+ s.test_files = [
38
+ "test/fluffy_test.rb",
39
+ "test/test_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ else
48
+ end
49
+ else
50
+ end
51
+ end
data/lib/fluffy.rb ADDED
@@ -0,0 +1,330 @@
1
+ require File.join(File.dirname(__FILE__), 'fluffy/s3_path')
2
+ require File.join(File.dirname(__FILE__), 'fluffy/s3_io')
3
+
4
+ require 'right_aws'
5
+ require 'fileutils'
6
+ require 'pathname'
7
+
8
+ RealFile = File
9
+ #RealFileUtils = FileUtils
10
+ RealDir = Dir
11
+ #RealFileUtils::Dir = RealDir
12
+ #RealFileUtils::File = RealFile
13
+
14
+ module Fluffy
15
+ #module FileUtils
16
+ # extend self
17
+
18
+ # def mkdir_p(path)
19
+ # RealFileUtils.mkdir_p(path)
20
+ # end
21
+
22
+ # def rm(path)
23
+ # RealFileUtils.rm(path)
24
+ # end
25
+
26
+ # def rm_r(path)
27
+ # RealFileUtils.rm_r(path)
28
+ # end
29
+
30
+ # def rm_rf(path)
31
+ # RealFileUtils.rm_rf(path)
32
+ # end
33
+
34
+ # def ln_s(target, path)
35
+ # raise Errno::EEXIST, path if File.exists?(path)
36
+ # RealFileUtils.ln_s(target, path)
37
+ # end
38
+
39
+ # def cp(src, dest)
40
+ # RealFileUtils.cp(src, dest)
41
+ # #dst_file = FileSystem.find(dest)
42
+ # #src_file = FileSystem.find(src)
43
+
44
+ # #if !src_file
45
+ # # raise Errno::ENOENT, src
46
+ # #end
47
+
48
+ # #if File.directory? src_file
49
+ # # raise Errno::EISDIR, src
50
+ # #end
51
+
52
+ # #if dst_file and File.directory?(dst_file)
53
+ # # FileSystem.add(File.join(dest, src), src_file.entry.clone(dst_file))
54
+ # #else
55
+ # # FileSystem.delete(dest)
56
+ # # FileSystem.add(dest, src_file.entry.clone)
57
+ # #end
58
+ # end
59
+
60
+ # def cp_r(src, dest)
61
+ # RealFileUtils.cp_r(src, dest)
62
+ # ## This error sucks, but it conforms to the original Ruby
63
+ # ## method.
64
+ # #raise "unknown file type: #{src}" unless dir = FileSystem.find(src)
65
+
66
+ # #new_dir = FileSystem.find(dest)
67
+
68
+ # #if new_dir && !File.directory?(dest)
69
+ # # raise Errno::EEXIST, dest
70
+ # #end
71
+
72
+ # #if !new_dir && !FileSystem.find(dest+'/../')
73
+ # # raise Errno::ENOENT, dest
74
+ # #end
75
+
76
+ # ## This last bit is a total abuse and should be thought hard
77
+ # ## about and cleaned up.
78
+ # #if new_dir
79
+ # # if src[-2..-1] == '/.'
80
+ # # dir.values.each{|f| new_dir[f.name] = f.clone(new_dir) }
81
+ # # else
82
+ # # new_dir[dir.name] = dir.entry.clone(new_dir)
83
+ # # end
84
+ # #else
85
+ # # FileSystem.add(dest, dir.entry.clone)
86
+ # #end
87
+ # end
88
+
89
+ # def mv(src, dest)
90
+ # RealFileUtils.mv(src, dest)
91
+ # #if target = FileSystem.find(src)
92
+ # # FileSystem.add(dest, target.entry.clone)
93
+ # # FileSystem.delete(src)
94
+ # #else
95
+ # # raise Errno::ENOENT, src
96
+ # #end
97
+ # end
98
+
99
+ # def chown(user, group, list, options={})
100
+ # RealFileUtils.chown(user, group, list, options)
101
+ # #list = Array(list)
102
+ # #list.each do |f|
103
+ # # unless File.exists?(f)
104
+ # # raise Errno::ENOENT, f
105
+ # # end
106
+ # #end
107
+ # #list
108
+ # end
109
+
110
+ # def chown_R(user, group, list, options={})
111
+ # RealFileUtils.chown_R(user, group, list, options={})
112
+ # #chown(user, group, list, options={})
113
+ # end
114
+
115
+ # def touch(list, options={})
116
+ # RealFileUtils.touch(list, options)
117
+ # #Array(list).each do |f|
118
+ # # directory = File.dirname(f)
119
+ # # # FIXME this explicit check for '.' shouldn't need to happen
120
+ # # if File.exists?(directory) || directory == '.'
121
+ # # FileSystem.add(f, MockFile.new)
122
+ # # else
123
+ # # raise Errno::ENOENT, f
124
+ # # end
125
+ # #end
126
+ # end
127
+
128
+ # def cd(dir)
129
+ # RealFileUtils.cd(dir)
130
+ # end
131
+ # alias_method :chdir, :cd
132
+
133
+ # def method_missing(meth, *args, &block)
134
+ # RealFileUtils.send(meth, *args, &block)
135
+ # end
136
+ #end
137
+
138
+ class File
139
+ PATH_SEPARATOR = '/'
140
+
141
+ @@s3_paths = []
142
+
143
+ def self.register_s3(file_path, access_key_id, secret_access_key, bucket, start_path = '')
144
+ File.s3_paths << Fluffy::S3Path.new(File.expand_path(file_path), access_key_id, secret_access_key, bucket, start_path)
145
+ end
146
+
147
+ def self.s3_paths
148
+ return @@s3_paths
149
+ end
150
+
151
+ def self.join(*parts)
152
+ RealFile.join(parts)
153
+ end
154
+
155
+ def self.exist?(path)
156
+ Fluffy.cloud_runner(RealFile, :exist?, path) do |s3_path, key|
157
+ s3_path.bucket.key(key).exists? or self.directory?(path)
158
+ end
159
+ end
160
+
161
+ def self.const_missing(name)
162
+ RealFile.const_get(name)
163
+ end
164
+
165
+ class << self
166
+ alias_method :exists?, :exist?
167
+ end
168
+
169
+ def self.directory?(path)
170
+ Fluffy.cloud_runner(RealFile, :directory?, path) do |s3_path, key|
171
+ s3_path.bucket.keys('prefix' => key).map{|k| k.name}.any?{|keyname| keyname[key.length] && keyname[key.length].chr == '/'}
172
+ end
173
+ end
174
+
175
+ def self.symlink?(path)
176
+ RealFile.symlink?(path)
177
+ end
178
+
179
+ def self.file?(path)
180
+ RealFile.file?(path)
181
+ end
182
+
183
+ def self.expand_path(*args)
184
+ RealFile.expand_path(*args)
185
+ end
186
+
187
+ def self.basename(*args)
188
+ RealFile.basename(*args)
189
+ end
190
+
191
+ def self.dirname(path)
192
+ RealFile.dirname(path)
193
+ end
194
+
195
+ def self.readlink(path)
196
+ RealFile.readlink(path)
197
+ end
198
+
199
+ def self.open(path, mode='r', &block)
200
+ Fluffy.cloud_runner(RealFile, :open, path, block) do |s3_path, key|
201
+ s3io = Fluffy::S3Io.new(s3_path, key, mode)
202
+
203
+ if block_given?
204
+ block_val = yield(s3io)
205
+ s3io.close
206
+ return block_val
207
+ end
208
+
209
+ return s3io
210
+ end
211
+ end
212
+
213
+ def self.read(path)
214
+ open(path).read
215
+ end
216
+
217
+ def self.readlines(path)
218
+ open(path).readlines
219
+ end
220
+
221
+ def self.unlink(*filepaths)
222
+ filepaths.each do |filename|
223
+ Fluffy.cloud_runner(RealFile, :unlink, filename) do |s3_path, key|
224
+ s3_path.bucket.key(key).delete
225
+ end
226
+ end
227
+ end
228
+
229
+ def self.new(path, mode = nil)
230
+ Fluffy.cloud_runner(RealFile, :new, path) do |s3_path, key|
231
+ open(path, mode)
232
+ end
233
+ end
234
+
235
+ def self.chmod(mode, *files)
236
+ #FIXME: Make it work
237
+ end
238
+
239
+ def self.method_missing(meth, *args, &block)
240
+ RealFile.send(meth, *args, &block)
241
+ end
242
+ end
243
+
244
+ class Dir
245
+ def self.glob(pattern)
246
+ RealDir.glob(pattern)
247
+ #if pattern[-1,1] == '*'
248
+ # blk = proc { |entry| entry.to_s }
249
+ #else
250
+ # blk = proc { |entry| entry[1].parent.to_s }
251
+ #end
252
+ #(FileSystem.find(pattern) || []).map(&blk).uniq.sort
253
+ end
254
+
255
+ def self.[](pattern)
256
+ glob(pattern)
257
+ end
258
+
259
+ def self.entries(path)
260
+ RealDir.entries(path)
261
+ end
262
+
263
+ def self.chdir(dir, &blk)
264
+ RealDir.chdir(dir, blk)
265
+ end
266
+
267
+ def self.unlink(dirpath)
268
+ Fluffy.cloud_runner(self, :unlink, dirpath) do |s3_path, key|
269
+ s3_path.bucket.key(key).delete
270
+ end
271
+ end
272
+
273
+ class << self
274
+ alias_method :rmdir, :unlink
275
+ alias_method :delete, :unlink
276
+ end
277
+
278
+ def self.method_missing(meth, *args, &block)
279
+ RealDir.send(meth, *args, &block)
280
+ end
281
+ end
282
+
283
+ def self.cloud_runner(klass, method_name, filename, method_block=nil, &block)
284
+ expanded_filename = File.expand_path(filename)
285
+ if s3_path = File.s3_paths.find {|path| expanded_filename =~ /^#{path.file_path}/}
286
+ starting_position = s3_path.file_path.length + 1
287
+ if expanded_filename.length > starting_position
288
+ key = expanded_filename[starting_position .. -1]
289
+ yield(s3_path, File.join(s3_path.start_path, key).gsub(/^\W/, ''))
290
+ elsif method_name == :directory?
291
+ return true
292
+ else
293
+ raise 'You must specify a key'
294
+ end
295
+ else
296
+ klass.send(method_name, filename, &method_block)
297
+ end
298
+ end
299
+ end
300
+
301
+ Object.class_eval do
302
+ remove_const(:Dir)
303
+ remove_const(:File)
304
+ #remove_const(:FileUtils)
305
+ end
306
+
307
+ #FileUtils = Fluffy::FileUtils
308
+ File = Fluffy::File
309
+ Dir = Fluffy::Dir
310
+
311
+ ## File
312
+ #
313
+ # lchmod
314
+ # lchown
315
+ # lstat
316
+ # chmod
317
+ # chown
318
+ # stat
319
+ # utime
320
+ # directory?
321
+ # rename
322
+ # unlink
323
+ # symlink
324
+
325
+ ## Dir
326
+ #
327
+ # []
328
+ # mkdir
329
+ # entries
330
+ # glob
@@ -0,0 +1,104 @@
1
+ module Fluffy
2
+ class S3Io < IO
3
+ attr_accessor :s3_path, :s3, :bucket, :key, :data, :mode, :pos
4
+ attr_reader :read_mode, :write_mode
5
+
6
+ def initialize(s3_path, key, mode)
7
+ self.s3_path = s3_path
8
+ self.key = self.s3_path.bucket.key(key)
9
+ self.data = self.key.data || ''
10
+ self.mode = mode
11
+
12
+ case self.mode.gsub(/b/, '') # We ignore the binary flag at this time
13
+ when 'r', 'r+'
14
+ self.pos = 0
15
+ @read_mode = true
16
+ @write_mode = !!(self.mode =~ /\+/)
17
+ when 'w', 'w+'
18
+ self.data = ''
19
+ self.pos = 0
20
+ @read_mode = !!(self.mode =~ /\+/)
21
+ @write_mode = true
22
+ when 'a', 'a+'
23
+ self.pos = self.data.length
24
+ @read_mode = !!(self.mode =~ /\+/)
25
+ @write_mode = true
26
+ end
27
+ end
28
+
29
+ def eof
30
+ eof?
31
+ end
32
+
33
+ def eof?
34
+ self.pos >= self.data.length
35
+ end
36
+
37
+ def each(&block)
38
+ until self.eof?
39
+ yield(self.gets)
40
+ end
41
+ end
42
+
43
+ def each_line(&block)
44
+ self.each(block)
45
+ end
46
+
47
+ def read(length = nil)
48
+ if length
49
+ return nil if self.eof?
50
+ data = self.data[self.pos, length]
51
+ self.pos += length
52
+ else
53
+ data = self.data[self.pos .. -1]
54
+ self.pos = self.data.length
55
+ end
56
+
57
+ return data
58
+ end
59
+
60
+ def write(string = nil)
61
+ string ||= $_
62
+ string << $\ if $\
63
+ self.data[self.pos, string.length] = string
64
+ self.pos += string.length
65
+ return nil
66
+ end
67
+ alias_method :print, :write
68
+ alias_method :<<, :write
69
+
70
+ def puts(*strings)
71
+ strings.each do |string|
72
+ string << "\n" unless string =~ /\n$/
73
+ self.print(string)
74
+ end
75
+ self.flush
76
+ return nil
77
+ end
78
+
79
+ def readline
80
+ self.gets
81
+ end
82
+
83
+ def readlines
84
+ return self.data.split("\n")
85
+ end
86
+
87
+ def gets(sep_string = "\n")
88
+ sep_string = "\n\n" if sep_string == ''
89
+
90
+ end_pos = self.data.index(sep_string, self.pos) || self.data.length
91
+
92
+ $_ = self.read(end_pos - self.pos + 1)
93
+ end
94
+
95
+ def close
96
+ flush
97
+ end
98
+
99
+ def flush
100
+ self.key.put(self.data)
101
+ self
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,14 @@
1
+ module Fluffy
2
+ class S3Path
3
+ attr_accessor :file_path, :access_key_id, :secret_access_key, :bucket, :start_path, :s3
4
+
5
+ def initialize(file_path, access_key_id, secret_access_key, bucket, start_path = '')
6
+ self.file_path = file_path
7
+ self.access_key_id = access_key_id
8
+ self.secret_access_key = secret_access_key
9
+ self.s3 = RightAws::S3.new(self.access_key_id, self.secret_access_key)
10
+ self.bucket = self.s3.bucket(bucket)
11
+ self.start_path = start_path
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class FluffyTest < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'fluffy'
8
+
9
+ class Test::Unit::TestCase
10
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluffy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Ben Wyrosdick
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-24 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: ben@commonthread.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - .document
27
+ - .gitignore
28
+ - LICENSE
29
+ - README.rdoc
30
+ - Rakefile
31
+ - VERSION
32
+ - features/fluffy.feature
33
+ - features/step_definitions/fluffy_steps.rb
34
+ - features/support/env.rb
35
+ - fluffy.gemspec
36
+ - lib/fluffy.rb
37
+ - lib/fluffy/s3_io.rb
38
+ - lib/fluffy/s3_path.rb
39
+ - test/fluffy_test.rb
40
+ - test/test_helper.rb
41
+ has_rdoc: true
42
+ homepage: http://github.com/commonthread/fluffy
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.3.4
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Makes writing to S3 tranparent using File
69
+ test_files:
70
+ - test/fluffy_test.rb
71
+ - test/test_helper.rb