require 'fileutils' RealFile = File RealFileUtils = FileUtils RealDir = Dir 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 def ln_s(target, path) FileSystem.add(path, MockSymlink.new(target)) end def cp_r(src, dest) if dir = FileSystem.find(src) FileSystem.add(dest, dir.entry) end end def mv(src, dest) if target = FileSystem.find(src) FileSystem.add(dest, target.entry) FileSystem.delete(src) end end end class File PATH_SEPARATOR = '/' def self.join(*parts) parts * PATH_SEPARATOR end def self.exists?(path) FileSystem.find(path) || false end def self.directory?(path) FileSystem.find(path).is_a? MockDir end def self.symlink?(path) FileSystem.find(path).is_a? MockSymlink end def self.file?(path) FileSystem.find(path).is_a? MockFile 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) if block_given? yield new(path, mode) else new(path, mode) end end attr_reader :path def initialize(path, mode) @path = path @mode = mode end def read end def puts(content) write(content + "\n") end def write(content) FileUtils.mkdir_p(File.dirname(path)) end alias_method :print, :write end class Dir def self.glob(pattern) FileSystem.find(pattern).map { |entry| entry.to_s} end def self.[](pattern) glob(pattern) end end module FileSystem extend self def fs @fs ||= MockDir.new('.') end def clear @fs = nil end def files fs.values end def find(path) parts = path_parts(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(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] : RealDir.glob(pattern) files.each do |f| if RealFile.file?(f) FileUtils.mkdir_p(File.dirname(f)) File.open(f, 'w') do |f| f.puts RealFile.read(f) 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 path_parts(path) path.split(File::PATH_SEPARATOR).reject { |part| part.empty? } end end class MockFile attr_accessor :name, :parent def initialize(name = nil, parent = nil) @name = name @parent = parent end def entry self 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 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 Object.class_eval do remove_const(:Dir) remove_const(:File) remove_const(:FileUtils) end File = FakeFS::File FileUtils = FakeFS::FileUtils Dir = FakeFS::Dir