diff --git a/Rakefile b/Rakefile index 61c7e59..fe0410d 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,10 @@ -task :default do - Dir['test/*_test.rb'].each { |file| require file } +desc "Run tests" +task :test do + Dir['test/**/*_test.rb'].each { |file| require file } end +task :default => :test + begin require 'jeweler' diff --git a/lib/fakefs/fake/dir.rb b/lib/fakefs/fake/dir.rb index 5ca8ca1..c75a57b 100644 --- a/lib/fakefs/fake/dir.rb +++ b/lib/fakefs/fake/dir.rb @@ -33,5 +33,13 @@ module FakeFS name end end + + def delete(node = self) + if node == self + parent.delete(self) + else + super(node.name) + end + end end end diff --git a/lib/fakefs/fake/file.rb b/lib/fakefs/fake/file.rb index c8ce6df..2c3074b 100644 --- a/lib/fakefs/fake/file.rb +++ b/lib/fakefs/fake/file.rb @@ -1,16 +1,54 @@ module FakeFS class FakeFile - attr_accessor :name, :parent, :content + attr_accessor :name, :parent + + class Inode + def initialize(file_owner) + @content = "" + @links = [file_owner] + end + + attr_accessor :content + attr_accessor :links + + def link(file) + links << file unless links.include?(file) + file.inode = self + end + + def unlink(file) + links.delete(file) + end + end def initialize(name = nil, parent = nil) - @name = name + @name = name @parent = parent - @content = '' + @inode = Inode.new(self) + end + + attr_accessor :inode + + def content + @inode.content + end + + def content=(str) + @inode.content = str + end + + def links + @inode.links + end + + def link(other_file) + @inode.link(other_file) end def clone(parent = nil) clone = super() clone.parent = parent if parent + clone.inode = inode.clone clone end @@ -25,5 +63,10 @@ module FakeFS def to_s File.join(parent.to_s, name) end + + def delete + inode.unlink(self) + parent.delete(self) + end end end diff --git a/lib/fakefs/fake/symlink.rb b/lib/fakefs/fake/symlink.rb index aef056e..81d6c3c 100644 --- a/lib/fakefs/fake/symlink.rb +++ b/lib/fakefs/fake/symlink.rb @@ -15,12 +15,18 @@ module FakeFS FileSystem.find(target) end - def method_missing(*args, &block) - entry.send(*args, &block) + def delete + parent.delete(self) end def respond_to?(method) entry.respond_to?(method) end + + private + + def method_missing(*args, &block) + entry.send(*args, &block) + end end end diff --git a/lib/fakefs/file.rb b/lib/fakefs/file.rb index 7a1f9b3..64c5afe 100644 --- a/lib/fakefs/file.rb +++ b/lib/fakefs/file.rb @@ -2,6 +2,31 @@ module FakeFS class File PATH_SEPARATOR = '/' + MODES = [ + READ_ONLY = "r", + READ_WRITE = "r+", + WRITE_ONLY = "w", + READ_WRITE_TRUNCATE = "w+", + APPEND_WRITE_ONLY = "a", + APPEND_READ_WRITE = "a+" + ] + + FILE_CREATION_MODES = MODES - [READ_ONLY, READ_WRITE] + + READ_ONLY_MODES = [ + READ_ONLY + ] + + WRITE_ONLY_MODES = [ + WRITE_ONLY, + APPEND_WRITE_ONLY + ] + + TRUNCATION_MODES = [ + WRITE_ONLY, + READ_WRITE_TRUNCATE + ] + def self.extname(path) RealFile.extname(path) end @@ -69,7 +94,7 @@ module FakeFS FileSystem.find(symlink.target).to_s end - def self.open(path, mode='r', perm = 0644) + def self.open(path, mode=READ_ONLY, perm = 0644) if block_given? yield new(path, mode, perm) else @@ -89,13 +114,86 @@ module FakeFS def self.readlines(path) read(path).split("\n") end + + def self.link(source, dest) + if directory?(source) + raise Errno::EPERM, "Operation not permitted - #{source} or #{dest}" + end + + if !exists?(source) + raise Errno::ENOENT, "No such file or directory - #{source} or #{dest}" + end + + if exists?(dest) + raise Errno::EEXIST, "File exists - #{source} or #{dest}" + end + + source = FileSystem.find(source) + dest = FileSystem.add(dest, source.entry.clone) + source.link(dest) + + 0 + end + + def self.delete(file_name, *additional_file_names) + if !exists?(file_name) + raise Errno::ENOENT, "No such file or directory - #{file_name}" + end + + FileUtils.rm(file_name) + + additional_file_names.each do |file_name| + FileUtils.rm(file_name) + end + + additional_file_names.size + 1 + end + + class << self + alias_method :unlink, :delete + end + + def self.symlink(source, dest) + FileUtils.ln_s(source, dest) + end + + def self.stat(file) + File::Stat.new(file) + end + + class Stat + def initialize(file) + if !File.exists?(file) + raise(Errno::ENOENT, "No such file or directory - #{file}") + end + + @file = file + end + + def symlink? + File.symlink?(@file) + end + + def directory? + File.directory?(@file) + end + + def nlink + FileSystem.find(@file).links.size + end + end attr_reader :path - def initialize(path, mode = nil, perm = nil) + + def initialize(path, mode = READ_ONLY, perm = nil) @path = path @mode = mode @file = FileSystem.find(path) @open = true + + check_valid_mode + file_creation_mode? ? create_missing_file : check_file_existence! + truncate_file if truncation_mode? end def close @@ -103,7 +201,9 @@ module FakeFS end def read - raise IOError.new('closed stream') unless @open + raise IOError, 'closed stream' unless @open + raise IOError, 'not opened for reading' if write_only? + @file.content end @@ -118,11 +218,8 @@ module FakeFS end def write(content) - raise IOError.new('closed stream') unless @open - - if !File.exists?(@path) - @file = FileSystem.add(path, FakeFile.new) - end + raise IOError, 'closed stream' unless @open + raise IOError, 'not open for writing' if read_only? @file.content += content end @@ -130,5 +227,49 @@ module FakeFS alias_method :<<, :write def flush; self; end + + private + + def check_file_existence! + unless @file + raise Errno::ENOENT, "No such file or directory - #{@file}" + end + end + + def check_valid_mode + if !mode_in?(MODES) + raise ArgumentError, "illegal access mode #{@mode}" + end + end + + def read_only? + mode_in? READ_ONLY_MODES + end + + def file_creation_mode? + mode_in? FILE_CREATION_MODES + end + + def write_only? + mode_in? WRITE_ONLY_MODES + end + + def truncation_mode? + mode_in? TRUNCATION_MODES + end + + def mode_in?(list) + list.include?(@mode) + end + + def create_missing_file + if !File.exists?(@path) + @file = FileSystem.add(path, FakeFile.new) + end + end + + def truncate_file + @file.content = "" + end end end diff --git a/lib/fakefs/file_system.rb b/lib/fakefs/file_system.rb index f24a50b..09af802 100644 --- a/lib/fakefs/file_system.rb +++ b/lib/fakefs/file_system.rb @@ -54,7 +54,7 @@ module FakeFS files.each do |f| if RealFile.file?(f) FileUtils.mkdir_p(File.dirname(f)) - File.open(f, 'w') do |g| + File.open(f, File::WRITE_ONLY) do |g| g.print RealFile.open(f){|h| h.read } end elsif RealFile.directory?(f) @@ -66,8 +66,8 @@ module FakeFS end def delete(path) - if dir = FileSystem.find(path) - dir.parent.delete(dir.name) + if node = FileSystem.find(path) + node.delete end end diff --git a/test/fake/file_test.rb b/test/fake/file_test.rb new file mode 100644 index 0000000..dcd87af --- /dev/null +++ b/test/fake/file_test.rb @@ -0,0 +1,88 @@ +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', 'lib') +require 'fakefs/safe' +require 'test/unit' + +class FakeFileTest < Test::Unit::TestCase + include FakeFS + + def setup + FileSystem.clear + + @file = FakeFile.new + end + + def test_fake_file_has_empty_content_by_default + assert_equal "", @file.content + end + + def test_fake_file_can_read_and_write_to_content + @file.content = "foobar" + assert_equal "foobar", @file.content + end + + def test_fake_file_has_1_link_by_default + assert_equal [@file], @file.links + end + + def test_fake_file_can_create_link + other_file = FakeFile.new + + @file.link(other_file) + + assert_equal [@file, other_file], @file.links + end + + def test_fake_file_wont_add_link_to_same_file_twice + other_file = FakeFile.new + + @file.link other_file + @file.link other_file + + assert_equal [@file, other_file], @file.links + end + + def test_links_are_mutual + other_file = FakeFile.new + + @file.link(other_file) + + assert_equal [@file, other_file], other_file.links + end + + def test_can_link_multiple_files + file_two = FakeFile.new + file_three = FakeFile.new + + @file.link file_two + @file.link file_three + + assert_equal [@file, file_two, file_three], @file.links + assert_equal [@file, file_two, file_three], file_two.links + assert_equal [@file, file_two, file_three], file_three.links + end + + def test_links_share_same_content + other_file = FakeFile.new + + @file.link other_file + + @file.content = "foobar" + + assert_equal "foobar", other_file.content + end + + def test_clone_creates_new_inode + clone = @file.clone + assert !clone.inode.equal?(@file.inode) + end + + def test_cloning_does_not_use_same_content_object + clone = @file.clone + + clone.content = "foo" + @file.content = "bar" + + assert_equal "foo", clone.content + assert_equal "bar", @file.content + end +end diff --git a/test/fake/symlink_test.rb b/test/fake/symlink_test.rb new file mode 100644 index 0000000..2c59c69 --- /dev/null +++ b/test/fake/symlink_test.rb @@ -0,0 +1,11 @@ +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', 'lib') +require 'fakefs/safe' +require 'test/unit' + +class FakeSymlinkTest < Test::Unit::TestCase + include FakeFS + + def test_symlink_has_method_missing_as_private + assert FakeSymlink.private_instance_methods.include?("method_missing") + end +end \ No newline at end of file diff --git a/test/fakefs_test.rb b/test/fakefs_test.rb index 0ba1f46..0d06956 100644 --- a/test/fakefs_test.rb +++ b/test/fakefs_test.rb @@ -96,6 +96,92 @@ class FakeFSTest < Test::Unit::TestCase assert File.exists?(path) end + def test_file_opens_in_read_only_mode + File.open("foo", "w") { |f| f << "foo" } + + f = File.open("foo") + + assert_raises(IOError) do + f << "bar" + end + end + + def test_file_opens_in_invalid_mode + FileUtils.touch("foo") + + assert_raises(ArgumentError) do + File.open("foo", "an_illegal_mode") + end + end + + def test_raises_error_when_cannot_find_file_in_read_mode + assert_raises(Errno::ENOENT) do + File.open("does_not_exist", "r") + end + end + + def test_raises_error_when_cannot_find_file_in_read_write_mode + assert_raises(Errno::ENOENT) do + File.open("does_not_exist", "r+") + end + end + + def test_creates_files_in_write_only_mode + File.open("foo", "w") + assert File.exists?("foo") + end + + def test_creates_files_in_read_write_truncate_mode + File.open("foo", "w+") + assert File.exists?("foo") + end + + def test_creates_files_in_append_write_only + File.open("foo", "a") + assert File.exists?("foo") + end + + def test_creates_files_in_append_read_write + File.open("foo", "a+") + assert File.exists?("foo") + end + + def test_file_in_write_only_raises_error_when_reading + FileUtils.touch("foo") + + f = File.open("foo", "w") + + assert_raises(IOError) do + f.read + end + end + + def test_file_in_write_mode_truncates_existing_file + File.open("foo", "w") { |f| f << "contents" } + + f = File.open("foo", "w") + + assert_equal "", File.read("foo") + end + + def test_file_in_read_write_truncation_mode_truncates_file + File.open("foo", "w") { |f| f << "foo" } + + f = File.open("foo", "w+") + + assert_equal "", File.read("foo") + end + + def test_file_in_append_write_only_raises_error_when_reading + FileUtils.touch("foo") + + f = File.open("foo", "a") + + assert_raises(IOError) do + f.read + end + end + def test_can_read_files_once_written path = '/path/to/file.txt' File.open(path, 'w') do |f| @@ -532,6 +618,14 @@ class FakeFSTest < Test::Unit::TestCase assert_equal 'works', File.open('new/nother') { |f| f.read } end + def test_can_symlink_through_file + FileUtils.touch("/foo") + + File.symlink("/foo", "/bar") + + assert File.symlink?("/bar") + end + def test_files_can_be_touched FileUtils.touch('touched_file') assert File.exists?('touched_file') @@ -827,6 +921,144 @@ class FakeFSTest < Test::Unit::TestCase def test_tmpdir assert Dir.tmpdir == "/tmp" end + + def test_hard_link_creates_file + FileUtils.touch("/foo") + + File.link("/foo", "/bar") + assert File.exists?("/bar") + end + + def test_hard_link_with_missing_file_raises_error + assert_raises(Errno::ENOENT) do + File.link("/foo", "/bar") + end + end + + def test_hard_link_with_existing_destination_file + FileUtils.touch("/foo") + FileUtils.touch("/bar") + + assert_raises(Errno::EEXIST) do + File.link("/foo", "/bar") + end + end + + def test_hard_link_returns_0_when_successful + FileUtils.touch("/foo") + + assert_equal 0, File.link("/foo", "/bar") + end + + def test_hard_link_returns_duplicate_file + File.open("/foo", "w") { |x| x << "some content" } + + File.link("/foo", "/bar") + assert_equal "some content", File.read("/bar") + end + + def test_hard_link_with_directory_raises_error + Dir.mkdir "/foo" + + assert_raises(Errno::EPERM) do + File.link("/foo", "/bar") + end + end + + def test_file_stat_returns_file_stat_object + FileUtils.touch("/foo") + assert_equal File::Stat, File.stat("/foo").class + end + + def test_can_delete_file_with_delete + FileUtils.touch("/foo") + + File.delete("/foo") + + assert !File.exists?("/foo") + end + + def test_can_delete_multiple_files_with_delete + FileUtils.touch("/foo") + FileUtils.touch("/bar") + + File.delete("/foo", "/bar") + + assert !File.exists?("/foo") + assert !File.exists?("/bar") + end + + def test_delete_raises_argument_error_with_no_filename_given + assert_raises ArgumentError do + File.delete + end + end + + def test_delete_returns_number_one_when_given_one_arg + FileUtils.touch("/foo") + + assert_equal 1, File.delete("/foo") + end + + def test_delete_returns_number_two_when_given_two_args + FileUtils.touch("/foo") + FileUtils.touch("/bar") + + assert_equal 2, File.delete("/foo", "/bar") + end + + def test_delete_raises_error_when_first_file_does_not_exist + assert_raises Errno::ENOENT do + File.delete("/foo") + end + end + + def test_delete_does_not_raise_error_when_second_file_does_not_exist + FileUtils.touch("/foo") + + assert_nothing_raised do + File.delete("/foo", "/bar") + end + end + + def test_unlink_is_alias_for_delete + assert_equal File.method(:unlink), File.method(:delete) + end + + def test_unlink_removes_only_one_file_content + File.open("/foo", "w") { |f| f << "some_content" } + File.link("/foo", "/bar") + + File.unlink("/bar") + File.read("/foo") == "some_content" + end + + def test_link_reports_correct_stat_info_after_unlinking + File.open("/foo", "w") { |f| f << "some_content" } + File.link("/foo", "/bar") + + File.unlink("/bar") + assert_equal 1, File.stat("/foo").nlink + end + + def test_delete_works_with_symlink + FileUtils.touch("/foo") + File.symlink("/foo", "/bar") + + File.unlink("/bar") + + assert File.exists?("/foo") + assert !File.exists?("/bar") + end + + def test_delete_works_with_symlink_source + FileUtils.touch("/foo") + File.symlink("/foo", "/bar") + + File.unlink("/foo") + + assert !File.exists?("/foo") + end def here(fname) RealFile.expand_path(RealFile.dirname(__FILE__)+'/'+fname) diff --git a/test/file/stat_test.rb b/test/file/stat_test.rb new file mode 100644 index 0000000..4121774 --- /dev/null +++ b/test/file/stat_test.rb @@ -0,0 +1,70 @@ +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', 'lib') +require 'fakefs/safe' +require 'test/unit' + +class FileStatTest < Test::Unit::TestCase + include FakeFS + + def setup + FileSystem.clear + end + + def touch(*args) + FileUtils.touch(*args) + end + + def ln_s(*args) + FileUtils.ln_s(*args) + end + + def mkdir(*args) + Dir.mkdir(*args) + end + + def ln(*args) + File.link(*args) + end + + def test_file_stat_init_with_non_existant_file + assert_raises(Errno::ENOENT) do + File::Stat.new("/foo") + end + end + + def test_symlink_should_be_true_when_symlink + touch("/foo") + ln_s("/foo", "/bar") + + assert File::Stat.new("/bar").symlink? + end + + def test_symlink_should_be_false_when_not_a_symlink + FileUtils.touch("/foo") + + assert !File::Stat.new("/foo").symlink? + end + + def test_should_return_false_for_directory_when_not_a_directory + FileUtils.touch("/foo") + + assert !File::Stat.new("/foo").directory? + end + + def test_should_return_true_for_directory_when_a_directory + mkdir "/foo" + + assert File::Stat.new("/foo").directory? + end + + def test_one_file_has_hard_link + touch "testfile" + assert_equal 1, File.stat("testfile").nlink + end + + def test_two_hard_links_show_nlinks_as_two + touch "testfile" + ln "testfile", "testfile.bak" + + assert_equal 2, File.stat("testfile").nlink + end +end