diff --git a/Rakefile b/Rakefile index c96fc5a..d11e41f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,3 @@ task :default do - exec "ruby test/fakefs_test.rb" + Dir['test/*_test.rb'].each { |file| require file } end diff --git a/lib/fakefs.rb b/lib/fakefs.rb index 0d85386..f8be311 100644 --- a/lib/fakefs.rb +++ b/lib/fakefs.rb @@ -1,447 +1,6 @@ require 'fileutils' require 'pathname' - -RealFile = File -RealFileUtils = FileUtils -RealDir = Dir -RealFileUtils::Dir = RealDir -RealFileUtils::File = RealFile - -module FakeFS - module FileUtils - extend self - - def mkdir_p(path) - FileSystem.add(path, MockDir.new) - end - - def rm(path) - FileSystem.delete(path) - end - alias_method :rm_rf, :rm - alias_method :rm_r, :rm - - def ln_s(target, path) - raise Errno::EEXIST, path if FileSystem.find(path) - FileSystem.add(path, MockSymlink.new(target)) - end - - def cp(src, dest) - dst_file = FileSystem.find(dest) - src_file = FileSystem.find(src) - - if !src_file - raise Errno::ENOENT, src - end - - if File.directory? src_file - raise Errno::EISDIR, src - end - - if dst_file and File.directory?(dst_file) - FileSystem.add(File.join(dest, src), src_file.entry.clone(dst_file)) - else - FileSystem.delete(dest) - FileSystem.add(dest, src_file.entry.clone) - end - end - - def cp_r(src, dest) - # This error sucks, but it conforms to the original Ruby - # method. - raise "unknown file type: #{src}" unless dir = FileSystem.find(src) - - new_dir = FileSystem.find(dest) - - if new_dir && !File.directory?(dest) - raise Errno::EEXIST, dest - end - - if !new_dir && !FileSystem.find(dest+'/../') - raise Errno::ENOENT, dest - end - - # This last bit is a total abuse and should be thought hard - # about and cleaned up. - if new_dir - if src[-2..-1] == '/.' - dir.values.each{|f| new_dir[f.name] = f.clone(new_dir) } - else - new_dir[dir.name] = dir.entry.clone(new_dir) - end - else - FileSystem.add(dest, dir.entry.clone) - end - end - - def mv(src, dest) - if target = FileSystem.find(src) - FileSystem.add(dest, target.entry.clone) - FileSystem.delete(src) - else - raise Errno::ENOENT, src - end - end - - def chown(user, group, list, options={}) - list = Array(list) - list.each do |f| - unless File.exists?(f) - raise Errno::ENOENT, f - end - end - list - end - - def chown_R(user, group, list, options={}) - chown(user, group, list, options={}) - end - - def touch(list, options={}) - Array(list).each do |f| - directory = File.dirname(f) - # FIXME this explicit check for '.' shouldn't need to happen - if File.exists?(directory) || directory == '.' - FileSystem.add(f, MockFile.new) - else - raise Errno::ENOENT, f - end - end - end - - def cd(dir) - FileSystem.chdir(dir) - end - alias_method :chdir, :cd - end - - class File - PATH_SEPARATOR = '/' - - def self.join(*parts) - parts * PATH_SEPARATOR - end - - def self.exist?(path) - !!FileSystem.find(path) - end - - class << self - alias_method :exists?, :exist? - end - - def self.directory?(path) - if path.respond_to? :entry - path.entry.is_a? MockDir - else - result = FileSystem.find(path) - result ? result.entry.is_a?(MockDir) : false - end - end - - def self.symlink?(path) - if path.respond_to? :entry - path.is_a? MockSymlink - else - FileSystem.find(path).is_a? MockSymlink - end - end - - def self.file?(path) - if path.respond_to? :entry - path.entry.is_a? MockFile - else - result = FileSystem.find(path) - result ? result.entry.is_a?(MockFile) : false - end - end - - def self.expand_path(*args) - RealFile.expand_path(*args) - end - - def self.basename(*args) - RealFile.basename(*args) - end - - def self.dirname(path) - RealFile.dirname(path) - end - - def self.readlink(path) - symlink = FileSystem.find(path) - FileSystem.find(symlink.target).to_s - end - - def self.open(path, mode='r') - if block_given? - yield new(path, mode) - else - new(path, mode) - end - end - - def self.read(path) - file = new(path) - if file.exists? - file.read - else - raise Errno::ENOENT - end - end - - def self.readlines(path) - read(path).split("\n") - end - - attr_reader :path - def initialize(path, mode = nil) - @path = path - @mode = mode - @file = FileSystem.find(path) - @open = true - end - - def close - @open = false - end - - def read - raise IOError.new('closed stream') unless @open - @file.content - end - - def exists? - @file - end - - def puts(content) - write(content + "\n") - end - - def write(content) - raise IOError.new('closed stream') unless @open - - if !File.exists?(@path) - @file = FileSystem.add(path, MockFile.new) - end - - @file.content += content - end - alias_method :print, :write - alias_method :<<, :write - - def flush; self; end - end - - class Dir - def self.glob(pattern) - if pattern[-1,1] == '*' - blk = proc { |entry| entry.to_s } - else - blk = proc { |entry| entry[1].parent.to_s } - end - (FileSystem.find(pattern) || []).map(&blk).uniq.sort - end - - def self.[](pattern) - glob(pattern) - end - - def self.chdir(dir, &blk) - FileSystem.chdir(dir, &blk) - end - end - - module FileSystem - extend self - - def dir_levels - @dir_levels ||= [] - end - - def fs - @fs ||= MockDir.new('.') - end - - def clear - @dir_levels = nil - @fs = nil - end - - def files - fs.values - end - - def find(path) - parts = path_parts(normalize_path(path)) - - target = parts[0...-1].inject(fs) do |dir, part| - dir[part] || {} - end - - case parts.last - when '*' - target.values - else - target[parts.last] - end - end - - def add(path, object=MockDir.new) - parts = path_parts(normalize_path(path)) - - d = parts[0...-1].inject(fs) do |dir, part| - dir[part] ||= MockDir.new(part, dir) - end - - object.name = parts.last - object.parent = d - d[parts.last] ||= object - end - - # copies directories and files from the real filesystem - # into our fake one - def clone(path) - path = File.expand_path(path) - pattern = File.join(path, '**', '*') - files = RealFile.file?(path) ? [path] : [path] + RealDir.glob(pattern, RealFile::FNM_DOTMATCH) - - files.each do |f| - if RealFile.file?(f) - FileUtils.mkdir_p(File.dirname(f)) - File.open(f, 'w') do |g| - g.print RealFile.open(f){|h| h.read } - end - elsif RealFile.directory?(f) - FileUtils.mkdir_p(f) - elsif RealFile.symlink?(f) - FileUtils.ln_s() - end - end - end - - def delete(path) - if dir = FileSystem.find(path) - dir.parent.delete(dir.name) - end - end - - def chdir(dir, &blk) - new_dir = find(dir) - dir_levels.push dir if blk - - raise Errno::ENOENT, dir unless new_dir - - dir_levels.push dir if !blk - blk.call if blk - ensure - dir_levels.pop if blk - end - - def path_parts(path) - path.split(File::PATH_SEPARATOR).reject { |part| part.empty? } - end - - def normalize_path(path) - if Pathname.new(path).absolute? - File.expand_path(path) - else - parts = dir_levels + [path] - File.expand_path(File.join(*parts)) - end - end - - def current_dir - find(normalize_path('.')) - end - end - - class MockFile - attr_accessor :name, :parent, :content - - def initialize(name = nil, parent = nil) - @name = name - @parent = parent - @content = '' - end - - def clone(parent = nil) - clone = super() - clone.parent = parent if parent - clone - end - - def entry - self - end - - def inspect - "(MockFile name:#{name.inspect} parent:#{parent.to_s.inspect} size:#{content.size})" - end - - def to_s - File.join(parent.to_s, name) - end - end - - class MockDir < Hash - attr_accessor :name, :parent - - def initialize(name = nil, parent = nil) - @name = name - @parent = parent - end - - def entry - self - end - - def inspect - "(MockDir name:#{name.inspect} parent:#{parent.to_s.inspect} size:#{size})" - end - - def clone(parent = nil) - clone = Marshal.load(Marshal.dump(self)) - clone.each do |key, value| - value.parent = clone - end - clone.parent = parent if parent - clone - end - - def to_s - if parent && parent.to_s != '.' - File.join(parent.to_s, name) - elsif parent && parent.to_s == '.' - "#{File::PATH_SEPARATOR}#{name}" - else - name - end - end - end - - class MockSymlink - attr_accessor :name, :target - alias_method :to_s, :name - - def initialize(target) - @target = target - end - - def inspect - "symlink(#{target.split('/').last})" - end - - def entry - FileSystem.find(target) - end - - def method_missing(*args, &block) - entry.send(*args, &block) - end - end -end +require 'fakefs/safe' Object.class_eval do remove_const(:Dir) diff --git a/lib/fakefs/safe.rb b/lib/fakefs/safe.rb new file mode 100644 index 0000000..3df0e50 --- /dev/null +++ b/lib/fakefs/safe.rb @@ -0,0 +1,470 @@ +RealFile = File +RealFileUtils = FileUtils +RealDir = Dir +RealFileUtils::Dir = RealDir +RealFileUtils::File = RealFile + +module FakeFS + def self.activate! + Object.class_eval do + remove_const(:Dir) + remove_const(:File) + remove_const(:FileUtils) + const_set(:Dir, FakeFS::Dir) + const_set(:File, FakeFS::File) + const_set(:FileUtils, FakeFS::FileUtils) + end + end + + def self.deactivate! + Object.class_eval do + remove_const(:Dir) + remove_const(:File) + remove_const(:FileUtils) + const_set(:Dir, RealDir) + const_set(:File, RealFile) + const_set(:FileUtils, RealFileUtils) + end + end + + module FileUtils + extend self + + def mkdir_p(path) + FileSystem.add(path, MockDir.new) + end + + def rm(path) + FileSystem.delete(path) + end + alias_method :rm_rf, :rm + alias_method :rm_r, :rm + + def ln_s(target, path) + raise Errno::EEXIST, path if FileSystem.find(path) + FileSystem.add(path, MockSymlink.new(target)) + end + + def cp(src, dest) + dst_file = FileSystem.find(dest) + src_file = FileSystem.find(src) + + if !src_file + raise Errno::ENOENT, src + end + + if File.directory? src_file + raise Errno::EISDIR, src + end + + if dst_file and File.directory?(dst_file) + FileSystem.add(File.join(dest, src), src_file.entry.clone(dst_file)) + else + FileSystem.delete(dest) + FileSystem.add(dest, src_file.entry.clone) + end + end + + def cp_r(src, dest) + # This error sucks, but it conforms to the original Ruby + # method. + raise "unknown file type: #{src}" unless dir = FileSystem.find(src) + + new_dir = FileSystem.find(dest) + + if new_dir && !File.directory?(dest) + raise Errno::EEXIST, dest + end + + if !new_dir && !FileSystem.find(dest+'/../') + raise Errno::ENOENT, dest + end + + # This last bit is a total abuse and should be thought hard + # about and cleaned up. + if new_dir + if src[-2..-1] == '/.' + dir.values.each{|f| new_dir[f.name] = f.clone(new_dir) } + else + new_dir[dir.name] = dir.entry.clone(new_dir) + end + else + FileSystem.add(dest, dir.entry.clone) + end + end + + def mv(src, dest) + if target = FileSystem.find(src) + FileSystem.add(dest, target.entry.clone) + FileSystem.delete(src) + else + raise Errno::ENOENT, src + end + end + + def chown(user, group, list, options={}) + list = Array(list) + list.each do |f| + unless File.exists?(f) + raise Errno::ENOENT, f + end + end + list + end + + def chown_R(user, group, list, options={}) + chown(user, group, list, options={}) + end + + def touch(list, options={}) + Array(list).each do |f| + directory = File.dirname(f) + # FIXME this explicit check for '.' shouldn't need to happen + if File.exists?(directory) || directory == '.' + FileSystem.add(f, MockFile.new) + else + raise Errno::ENOENT, f + end + end + end + + def cd(dir) + FileSystem.chdir(dir) + end + alias_method :chdir, :cd + end + + class File + PATH_SEPARATOR = '/' + + def self.join(*parts) + parts * PATH_SEPARATOR + end + + def self.exist?(path) + !!FileSystem.find(path) + end + + class << self + alias_method :exists?, :exist? + end + + def self.directory?(path) + if path.respond_to? :entry + path.entry.is_a? MockDir + else + result = FileSystem.find(path) + result ? result.entry.is_a?(MockDir) : false + end + end + + def self.symlink?(path) + if path.respond_to? :entry + path.is_a? MockSymlink + else + FileSystem.find(path).is_a? MockSymlink + end + end + + def self.file?(path) + if path.respond_to? :entry + path.entry.is_a? MockFile + else + result = FileSystem.find(path) + result ? result.entry.is_a?(MockFile) : false + end + end + + def self.expand_path(*args) + RealFile.expand_path(*args) + end + + def self.basename(*args) + RealFile.basename(*args) + end + + def self.dirname(path) + RealFile.dirname(path) + end + + def self.readlink(path) + symlink = FileSystem.find(path) + FileSystem.find(symlink.target).to_s + end + + def self.open(path, mode='r') + if block_given? + yield new(path, mode) + else + new(path, mode) + end + end + + def self.read(path) + file = new(path) + if file.exists? + file.read + else + raise Errno::ENOENT + end + end + + def self.readlines(path) + read(path).split("\n") + end + + attr_reader :path + def initialize(path, mode = nil) + @path = path + @mode = mode + @file = FileSystem.find(path) + @open = true + end + + def close + @open = false + end + + def read + raise IOError.new('closed stream') unless @open + @file.content + end + + def exists? + @file + end + + def puts(content) + write(content + "\n") + end + + def write(content) + raise IOError.new('closed stream') unless @open + + if !File.exists?(@path) + @file = FileSystem.add(path, MockFile.new) + end + + @file.content += content + end + alias_method :print, :write + alias_method :<<, :write + + def flush; self; end + end + + class Dir + def self.glob(pattern) + if pattern[-1,1] == '*' + blk = proc { |entry| entry.to_s } + else + blk = proc { |entry| entry[1].parent.to_s } + end + (FileSystem.find(pattern) || []).map(&blk).uniq.sort + end + + def self.[](pattern) + glob(pattern) + end + + def self.chdir(dir, &blk) + FileSystem.chdir(dir, &blk) + end + end + + module FileSystem + extend self + + def dir_levels + @dir_levels ||= [] + end + + def fs + @fs ||= MockDir.new('.') + end + + def clear + @dir_levels = nil + @fs = nil + end + + def files + fs.values + end + + def find(path) + parts = path_parts(normalize_path(path)) + + target = parts[0...-1].inject(fs) do |dir, part| + dir[part] || {} + end + + case parts.last + when '*' + target.values + else + target[parts.last] + end + end + + def add(path, object=MockDir.new) + parts = path_parts(normalize_path(path)) + + d = parts[0...-1].inject(fs) do |dir, part| + dir[part] ||= MockDir.new(part, dir) + end + + object.name = parts.last + object.parent = d + d[parts.last] ||= object + end + + # copies directories and files from the real filesystem + # into our fake one + def clone(path) + path = File.expand_path(path) + pattern = File.join(path, '**', '*') + files = RealFile.file?(path) ? [path] : [path] + RealDir.glob(pattern, RealFile::FNM_DOTMATCH) + + files.each do |f| + if RealFile.file?(f) + FileUtils.mkdir_p(File.dirname(f)) + File.open(f, 'w') do |g| + g.print RealFile.open(f){|h| h.read } + end + elsif RealFile.directory?(f) + FileUtils.mkdir_p(f) + elsif RealFile.symlink?(f) + FileUtils.ln_s() + end + end + end + + def delete(path) + if dir = FileSystem.find(path) + dir.parent.delete(dir.name) + end + end + + def chdir(dir, &blk) + new_dir = find(dir) + dir_levels.push dir if blk + + raise Errno::ENOENT, dir unless new_dir + + dir_levels.push dir if !blk + blk.call if blk + ensure + dir_levels.pop if blk + end + + def path_parts(path) + path.split(File::PATH_SEPARATOR).reject { |part| part.empty? } + end + + def normalize_path(path) + if Pathname.new(path).absolute? + File.expand_path(path) + else + parts = dir_levels + [path] + File.expand_path(File.join(*parts)) + end + end + + def current_dir + find(normalize_path('.')) + end + end + + class MockFile + attr_accessor :name, :parent, :content + + def initialize(name = nil, parent = nil) + @name = name + @parent = parent + @content = '' + end + + def clone(parent = nil) + clone = super() + clone.parent = parent if parent + clone + end + + def entry + self + end + + def inspect + "(MockFile name:#{name.inspect} parent:#{parent.to_s.inspect} size:#{content.size})" + end + + def to_s + File.join(parent.to_s, name) + end + end + + class MockDir < Hash + attr_accessor :name, :parent + + def initialize(name = nil, parent = nil) + @name = name + @parent = parent + end + + def entry + self + end + + def inspect + "(MockDir name:#{name.inspect} parent:#{parent.to_s.inspect} size:#{size})" + end + + def clone(parent = nil) + clone = Marshal.load(Marshal.dump(self)) + clone.each do |key, value| + value.parent = clone + end + clone.parent = parent if parent + clone + end + + def to_s + if parent && parent.to_s != '.' + File.join(parent.to_s, name) + elsif parent && parent.to_s == '.' + "#{File::PATH_SEPARATOR}#{name}" + else + name + end + end + end + + class MockSymlink + attr_accessor :name, :target + alias_method :to_s, :name + + def initialize(target) + @target = target + end + + def inspect + "symlink(#{target.split('/').last})" + end + + def entry + FileSystem.find(target) + end + + def method_missing(*args, &block) + entry.send(*args, &block) + end + end +end + +def FakeFS + return FakeFS unless block_given? + ::FakeFS.activate! + yield + ::FakeFS.deactivate! +end diff --git a/test/safe_test.rb b/test/safe_test.rb new file mode 100644 index 0000000..ac0f63f --- /dev/null +++ b/test/safe_test.rb @@ -0,0 +1,20 @@ +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') +require 'fakefs/safe' +require 'test/unit' + +class FakeFSSafeTest < Test::Unit::TestCase + def setup + FakeFS.deactivate! + end + + def test_FakeFS_method_does_not_intrude_on_global_namespace + path = '/path/to/file.txt' + + FakeFS do + File.open(path, 'w') { |f| f.write "Yatta!" } + assert File.exists?(path) + end + + assert ! File.exists?(path) + end +end