require "./spec_helper" require "../support/thread" require "wait_group" private def it_raises_on_null_byte(operation, file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) it "errors on #{operation}", file, line, end_line do expect_raises(ArgumentError, "String contains null byte") do block.call end end end private def normalize_permissions(permissions, *, directory) {% if flag?(:win32) %} normalized_permissions = 0o444 normalized_permissions |= 0o222 if permissions.bits_set?(0o200) normalized_permissions |= 0o111 if directory File::Permissions.new(normalized_permissions) {% else %} File::Permissions.new(permissions) {% end %} end # TODO: Find a better way to execute specs involving file permissions when # running as a privileged user. Compiling a program and running a separate # process would be a lot of overhead. private def pending_if_superuser! {% if flag?(:unix) %} if LibC.getuid == 0 pending! "Spec cannot run as superuser" end {% end %} end describe "File" do it "gets path" do path = datapath("test_file.txt") File.open(path) do |file| file.path.should eq(path) end end it "raises if opening a non-existent file" do with_tempfile("test_nonexistent.txt") do |file| expect_raises(File::NotFoundError) do File.open(file) end end end {% if LibC.has_method?(:mkfifo) && !flag?(:darwin) %} # interpreter doesn't support threads yet (#14287) pending_interpreted "can read/write fifo file without blocking" do path = File.tempname("chardev") ret = LibC.mkfifo(path, File::DEFAULT_CREATE_PERMISSIONS) raise RuntimeError.from_errno("mkfifo") unless ret == 0 rbuf = Bytes.new(5120) wbuf = Bytes.new(5120) Random::Secure.random_bytes(wbuf) {% if flag?(:execution_context) %} WaitGroup.wait do |wg| # one fiber may block on open(2) (depends on the event loop) but the # monitor thread will notice and move the scheduler to another thread, # unblocking the other fiber wg.spawn(name: "fifo:write") do File.open(path, "w") do |writer| 64.times { |i| writer.write(wbuf) } end end wg.spawn(name: "fifo:read") do File.open(path, "r") do |reader| 64.times { |i| reader.read_fully(rbuf) } end end end {% else %} # open(2) will block when opening a fifo file until another thread or # process also opened the file; so we must explicitly start a thread writer = nil thread = new_thread { writer = File.new(path, "w") } File.open(path, "r") do |reader| WaitGroup.wait do |wg| # opened fifo for read: wait for thread to open for write thread.join wg.spawn(name: "fifo:read") do 64.times { |i| reader.read_fully(rbuf) } end wg.spawn(name: "fifo:write") do 64.times { |i| writer.not_nil!.write(wbuf) } writer.not_nil!.close end end ensure writer.try(&.close) end {% end %} rbuf.should eq(wbuf) ensure File.delete(path) if path end {% end %} # This test verifies that the workaround for a win32 bug with the O_APPEND # equivalent with OVERLAPPED operations is working as expected. it "returns the actual position after append" do with_tempfile("delete-file.txt") do |filename| File.write(filename, "hello") File.open(filename, "a") do |file| file.tell.should eq(0) file.write "12345".to_slice file.tell.should eq(10) file.seek(5, IO::Seek::Set) file.write "6789".to_slice file.tell.should eq(14) end File.read(filename).should eq("hello123456789") end end it "reads entire file" do str = File.read datapath("test_file.txt") str.should eq("Hello World\n" * 20) end {% if flag?(:linux) %} it "reads entire file from proc virtual filesystem" do str1 = File.open "/proc/self/cmdline", &.gets_to_end str2 = File.read "/proc/self/cmdline" str2.should_not be_empty str2.should eq(str1) end {% end %} it "reads lines from file" do lines = File.read_lines datapath("test_file.txt") lines.size.should eq(20) lines.first.should eq("Hello World") end it "reads lines from file with chomp = false" do lines = File.read_lines datapath("test_file.txt"), chomp: false lines.size.should eq(20) lines.first.should eq("Hello World\n") end it "reads lines from file with each" do idx = 0 File.each_line(datapath("test_file.txt")) do |line| if idx == 0 line.should eq("Hello World") end idx += 1 end idx.should eq(20) end it "reads lines from file with each, chomp = false" do idx = 0 File.each_line(datapath("test_file.txt"), chomp: false) do |line| if idx == 0 line.should eq("Hello World\n") end idx += 1 end idx.should eq(20) end describe "empty?" do it "gives true when file is empty" do File.empty?(datapath("blank_test_file.txt")).should be_true end it "gives false when file is not empty" do File.empty?(datapath("test_file.txt")).should be_false end it "raises an error when the file does not exist" do filename = datapath("non_existing_file.txt") expect_raises(File::NotFoundError, "Unable to get file info: '#{filename.inspect_unquoted}'") do File.empty?(filename) end end # TODO: do we even want this? it "raises an error when a component of the path is a file" do expect_raises(File::Error, "Unable to get file info: '#{datapath("test_file.txt", "").inspect_unquoted}'") do File.empty?(datapath("test_file.txt", "")) end end end describe "exists?" do it "gives true" do File.exists?(datapath("test_file.txt")).should be_true end it "gives false" do File.exists?(datapath("non_existing_file.txt")).should be_false end it "gives false when a component of the path is a file" do File.exists?(datapath("dir", "test_file.txt", "")).should be_false end it "follows symlinks" do with_tempfile("good_symlink.txt", "bad_symlink.txt") do |good_path, bad_path| File.symlink(File.expand_path(datapath("test_file.txt")), good_path) File.symlink(File.expand_path(datapath("non_existing_file.txt")), bad_path) File.exists?(good_path).should be_true File.exists?(bad_path).should be_false end end it "gives true for null file (#15019)" do File.exists?(File::NULL).should be_true end end describe "file?" do it "gives true" do File.file?(datapath("test_file.txt")).should be_true end it "gives false with dir" do File.file?(datapath("dir")).should be_false end it "gives false when the file doesn't exist" do File.file?(datapath("non_existing_file.txt")).should be_false end it "gives false when a component of the path is a file" do File.file?(datapath("dir", "test_file.txt", "")).should be_false end end describe "directory?" do it "gives true" do File.directory?(datapath).should be_true end it "gives false" do File.directory?(datapath("test_file.txt")).should be_false end it "gives false when the directory doesn't exist" do File.directory?(datapath("non_existing")).should be_false end it "gives false when a component of the path is a file" do File.directory?(datapath("dir", "test_file.txt", "")).should be_false end end # hard links are practically unavailable on Android {% unless flag?(:android) %} describe "link" do it "creates a hard link" do with_tempfile("hard_link_source.txt", "hard_link_target.txt") do |in_path, out_path| File.write(in_path, "") File.link(in_path, out_path) File.exists?(out_path).should be_true File.symlink?(out_path).should be_false File.same?(in_path, out_path).should be_true end end end {% end %} describe "same?" do it "compares following symlinks only if requested" do file = datapath("test_file.txt") other = datapath("test_file.ini") with_tempfile("test_file_symlink.txt") do |symlink| File.symlink(File.realpath(file), symlink) File.same?(file, symlink).should be_false File.same?(file, symlink, follow_symlinks: true).should be_true File.same?(file, symlink, follow_symlinks: false).should be_false File.same?(file, other).should be_false end end end describe "symlink" do it "creates a symbolic link" do in_path = datapath("test_file.txt") with_tempfile("test_file_link.txt") do |out_path| File.symlink(File.realpath(in_path), out_path) File.symlink?(out_path).should be_true File.same?(in_path, out_path, follow_symlinks: true).should be_true end end it "works if destination contains forward slashes (#14520)" do with_tempfile("test_slash_dest.txt", "test_slash_link.txt") do |dest_path, link_path| File.write(dest_path, "hello") File.symlink("./test_slash_dest.txt", link_path) File.same?(dest_path, link_path, follow_symlinks: true).should be_true File.read(link_path).should eq("hello") end end end describe "symlink?" do it "gives false" do File.symlink?(datapath("test_file.txt")).should be_false File.symlink?(datapath("unknown_file.txt")).should be_false end it "gives false when the symlink doesn't exist" do File.symlink?(datapath("non_existing_file.txt")).should be_false end it "gives false when a component of the path is a file" do File.symlink?(datapath("dir", "test_file.txt", "")).should be_false end end describe ".readlink" do it "reads link" do File.readlink(datapath("symlink.txt")).should eq "test_file.txt" end it "raises when not a link" do expect_raises File::Error, "Cannot read link: '#{datapath("test_file.txt").inspect_unquoted}'" do File.readlink(datapath("test_file.txt")) end end it "raises when non-existent" do expect_raises File::NotFoundError, "Cannot read link: '#{datapath("nonexistent.txt").inspect_unquoted}'" do File.readlink(datapath("nonexistent.txt")) end end it "returns non-existent target" do with_tempfile("target-nonexistent") do |path| Dir.mkdir_p(path) Dir.cd(path) do File.symlink("nonexistent.txt", "symlink.txt") File.readlink("symlink.txt").should eq "nonexistent.txt" end end end it "raises when inaccessible" do # Crystal does not expose ways to make a file unreadable on Windows pending! if {{ flag?(:win32) }} pending_if_superuser! with_tempfile("readlink-inaccessible") do |path| Dir.mkdir_p(path) symlink = File.join(path, "symlink.txt") File.symlink("nonexistent.txt", symlink) File.chmod(path, File::Permissions::None) expect_raises File::AccessDeniedError, "Cannot read link: '#{symlink.inspect_unquoted}'" do File.readlink(symlink) end end end end describe ".readlink?" do it "reads link" do File.readlink?(datapath("symlink.txt")).should eq "test_file.txt" end it "returns nil when not a link" do File.readlink?(datapath("test_file.txt")).should be_nil end it "returns nil when non-existent" do File.readlink?(datapath("nonexistent.txt")).should be_nil end it "raises when target non-existent" do with_tempfile("target-nonexistent2") do |path| Dir.mkdir_p(path) Dir.cd(path) do File.symlink("nonexistent.txt", "symlink.txt") File.readlink?("symlink.txt").should eq "nonexistent.txt" end end end it "raises when inaccessible" do # Crystal does not expose ways to make a file unreadable on Windows pending! if {{ flag?(:win32) }} pending_if_superuser! with_tempfile("readlink-inaccessible2") do |path| Dir.mkdir_p(path) symlink = File.join(path, "symlink.txt") File.symlink("nonexistent.txt", symlink) File.chmod(path, File::Permissions::None) expect_raises File::AccessDeniedError, "Cannot read link: '#{symlink.inspect_unquoted}'" do File.readlink?(symlink) end end end end it "gets dirname" do File.dirname("/Users/foo/bar.cr").should eq("/Users/foo") File.dirname("foo").should eq(".") File.dirname("").should eq(".") File.dirname("/τελεία/łódź").should eq("/τελεία") end it "gets basename" do File.basename("/foo/bar/baz.cr").should eq("baz.cr") File.basename("/foo/").should eq("foo") File.basename("foo").should eq("foo") File.basename("").should eq("") File.basename("/").should eq("/") end it "gets basename removing suffix" do File.basename("/foo/bar/baz.cr", ".cr").should eq("baz") end it "gets extname" do File.extname("/foo/bar/a.cr").should eq(".cr") File.extname("/foo/bar/baz.cr").should eq(".cr") File.extname("/foo/bar/baz.cr.cz").should eq(".cz") File.extname("/foo/bar/.profile").should eq("") File.extname("/foo/bar/.profile.sh").should eq(".sh") File.extname("/foo/bar/foo.").should eq("") File.extname("/foo.bar/baz").should eq("") File.extname("a.cr").should eq(".cr") File.extname("test.cr").should eq(".cr") File.extname("test.cr.cz").should eq(".cz") File.extname(".test").should eq("") File.extname(".test.cr").should eq(".cr") File.extname(".test.cr.cz").should eq(".cz") File.extname("test").should eq("") File.extname("test.").should eq("") File.extname("").should eq("") end # There are more detailed specs for `Path#join` in path_spec.cr it "constructs a path from parts" do {% if flag?(:win32) %} File.join(["///foo", "bar"]).should eq("///foo\\bar") File.join(["///foo", "//bar"]).should eq("///foo//bar") File.join(["/foo/", "/bar"]).should eq("/foo/bar") File.join(["foo", "bar", "baz"]).should eq("foo\\bar\\baz") File.join(["foo", "//bar//", "baz///"]).should eq("foo//bar//baz///") File.join(["/foo/", "/bar/", "/baz/"]).should eq("/foo/bar/baz/") File.join(["", "foo"]).should eq("\\foo") File.join(["foo", ""]).should eq("foo\\") File.join(["", "", "foo"]).should eq("\\foo") File.join(["foo", "", "bar"]).should eq("foo\\bar") File.join(["foo", "", "", "bar"]).should eq("foo\\bar") File.join(["foo", "/", "bar"]).should eq("foo/bar") File.join(["foo", "/", "/", "bar"]).should eq("foo/bar") File.join(["/", "/foo", "/", "bar/", "/"]).should eq("/foo/bar/") File.join(["foo"]).should eq("foo") File.join("foo").should eq("foo") {% else %} File.join(["///foo", "bar"]).should eq("///foo/bar") File.join(["///foo", "//bar"]).should eq("///foo//bar") File.join(["/foo/", "/bar"]).should eq("/foo/bar") File.join(["foo", "bar", "baz"]).should eq("foo/bar/baz") File.join(["foo", "//bar//", "baz///"]).should eq("foo//bar//baz///") File.join(["/foo/", "/bar/", "/baz/"]).should eq("/foo/bar/baz/") File.join(["", "foo"]).should eq("/foo") File.join(["foo", ""]).should eq("foo/") File.join(["", "", "foo"]).should eq("/foo") File.join(["foo", "", "bar"]).should eq("foo/bar") File.join(["foo", "", "", "bar"]).should eq("foo/bar") File.join(["foo", "/", "bar"]).should eq("foo/bar") File.join(["foo", "/", "/", "bar"]).should eq("foo/bar") File.join(["/", "/foo", "/", "bar/", "/"]).should eq("/foo/bar/") File.join(["foo"]).should eq("foo") File.join("foo").should eq("foo") {% end %} end it "chown" do # changing owners requires special privileges, so we test that method calls do compile typeof(File.chown(".")) typeof(File.chown(".", uid: 1001, gid: 100, follow_symlinks: true)) File.open(File::NULL, "w") do |file| typeof(file.chown) typeof(file.chown(uid: 1001, gid: 100)) end end describe "chmod" do it "changes file permissions with class method" do path = datapath("chmod.txt") begin File.write(path, "") File.chmod(path, 0o775) File.info(path).permissions.should eq(normalize_permissions(0o775, directory: false)) ensure File.delete?(path) end end it "changes file permissions with instance method" do path = datapath("chmod.txt") begin File.open(path, "w") do |file| file.chmod(0o775) end File.info(path).permissions.should eq(normalize_permissions(0o775, directory: false)) ensure File.delete(path) if File.exists?(path) end end it "changes dir permissions" do path = datapath("chmod") begin Dir.mkdir(path, 0o775) File.chmod(path, 0o664) File.info(path).permissions.should eq(normalize_permissions(0o664, directory: true)) ensure Dir.delete?(path) end end it "can take File::Permissions" do path = datapath("chmod.txt") begin File.write(path, "") File.chmod(path, File::Permissions.flags(OwnerAll, GroupAll, OtherExecute, OtherRead)) File.info(path).permissions.should eq(normalize_permissions(0o775, directory: false)) ensure File.delete?(path) end end it "follows symlinks" do with_tempfile("chmod-destination.txt", "chmod-source.txt") do |source_path, target_path| File.write(source_path, "") File.symlink(File.realpath(source_path), target_path) File.symlink?(target_path).should be_true File.chmod(source_path, 0o664) File.chmod(target_path, 0o444) File.info(source_path).permissions.should eq(normalize_permissions(0o444, directory: false)) end end it "raises when destination doesn't exist" do expect_raises(File::NotFoundError, "Error changing permissions: '#{datapath("unknown_chmod_path.txt").inspect_unquoted}'") do File.chmod(datapath("unknown_chmod_path.txt"), 0o664) end end end long_path = "a" * 1000 describe ".info" do it "raises for too long pathname" do expect_raises(File::NotFoundError, /Unable to get file info: '#{long_path}': (File ?name too long|The system cannot find the path specified)/) do File.info(long_path) end end it "raises for invalid pathname" do expect_raises(File::NotFoundError, /Unable to get file info: '': (No such file or directory|The system cannot find the path specified)/) do File.info("") end end it "raises for invalid pathname" do expect_raises(File::NotFoundError, /Unable to get file info: '<': (No such file or directory|The filename, directory name, or volume label syntax is incorrect)/) do File.info("<") end end end describe ".info?" do it "returns nil for too long pathname" do File.info?(long_path).should be_nil end it "returns nil for invalid pathname" do File.info?("").should be_nil end it "returns nil for invalid pathname" do File.info?("<").should be_nil end end describe "File::Info" do it "gets for this file" do info = File.info(datapath("test_file.txt")) info.type.should eq(File::Type::File) end it "gets for this directory" do info = File.info(datapath) info.type.should eq(File::Type::Directory) end it "gets for a character device" do info = File.info(File::NULL) info.type.should eq(File::Type::CharacterDevice) end it "gets for a symlink" do file_path = File.expand_path(datapath("test_file.txt")) with_tempfile("symlink.txt") do |symlink_path| File.symlink(file_path, symlink_path) info = File.info(symlink_path, follow_symlinks: false) info.type.should eq(File::Type::Symlink) info = File.info(symlink_path, follow_symlinks: true) info.type.should_not eq(File::Type::Symlink) end end it "gets for open file" do File.open(datapath("test_file.txt"), "r") do |file| info = file.info info.type.should eq(File::Type::File) end end it "gets for pipe" do IO.pipe do |r, w| r.info.type.should eq(File::Type::Pipe) w.info.type.should eq(File::Type::Pipe) end end it "gets for non-existent file and raises" do expect_raises(File::NotFoundError, "Unable to get file info: 'non-existent'") do File.info("non-existent") end end it "gets mtime for new file" do with_tempfile("mtime") do |path| File.touch(path) File.open(path) do |file| file.info.modification_time.should be_close(Time.utc, 1.seconds) end File.info(path).modification_time.should be_close(Time.utc, 1.seconds) end end it "tests equal for the same file" do File.info(datapath("test_file.txt")).should eq(File.info(datapath("test_file.txt"))) end it "tests equal for the same directory" do File.info(datapath("dir")).should eq(File.info(datapath("dir"))) end it "tests unequal for different files" do File.info(datapath("test_file.txt")).should_not eq(File.info(datapath("test_file.ini"))) end it "tests unequal for file and directory" do File.info(datapath("dir")).should_not eq(File.info(datapath("test_file.txt"))) end describe ".executable?" do it "gives true" do crystal = Process.executable_path || pending! "Unable to locate compiler executable" File::Info.executable?(crystal).should be_true File.executable?(crystal).should be_true # deprecated end it "gives false" do File::Info.executable?(datapath("test_file.txt")).should be_false end it "gives false when the file doesn't exist" do File::Info.executable?(datapath("non_existing_file.txt")).should be_false end it "gives false when a component of the path is a file" do File::Info.executable?(datapath("dir", "test_file.txt", "")).should be_false end it "follows symlinks" do with_tempfile("good_symlink_x.txt", "bad_symlink_x.txt") do |good_path, bad_path| crystal = Process.executable_path || pending! "Unable to locate compiler executable" File.symlink(File.expand_path(crystal), good_path) File.symlink(File.expand_path(datapath("non_existing_file.txt")), bad_path) File::Info.executable?(good_path).should be_true File::Info.executable?(bad_path).should be_false end end end describe ".readable?" do it "gives true" do File::Info.readable?(datapath("test_file.txt")).should be_true File.readable?(datapath("test_file.txt")).should be_true # deprecated end it "gives false when the file doesn't exist" do File::Info.readable?(datapath("non_existing_file.txt")).should be_false end it "gives false when a component of the path is a file" do File::Info.readable?(datapath("dir", "test_file.txt", "")).should be_false end # win32 doesn't have a way to make files unreadable via chmod {% unless flag?(:win32) %} it "gives false when the file has no read permissions" do with_tempfile("unreadable.txt") do |path| File.write(path, "") File.chmod(path, 0o222) pending_if_superuser! File::Info.readable?(path).should be_false end end it "gives false when the file has no permissions" do with_tempfile("inaccessible.txt") do |path| File.write(path, "") File.chmod(path, 0o000) pending_if_superuser! File::Info.readable?(path).should be_false end end it "follows symlinks" do with_tempfile("good_symlink_r.txt", "bad_symlink_r.txt", "unreadable.txt") do |good_path, bad_path, unreadable| File.write(unreadable, "") File.chmod(unreadable, 0o222) pending_if_superuser! File.symlink(File.expand_path(datapath("test_file.txt")), good_path) File.symlink(File.expand_path(unreadable), bad_path) File::Info.readable?(good_path).should be_true File::Info.readable?(bad_path).should be_false end end {% end %} it "gives false when the symbolic link destination doesn't exist" do with_tempfile("missing_symlink_r.txt") do |missing_path| File.symlink(File.expand_path(datapath("non_existing_file.txt")), missing_path) File::Info.readable?(missing_path).should be_false end end end describe ".writable?" do it "gives true" do File::Info.writable?(datapath("test_file.txt")).should be_true File.writable?(datapath("test_file.txt")).should be_true # deprecated end it "gives false when the file doesn't exist" do File::Info.writable?(datapath("non_existing_file.txt")).should be_false end it "gives false when a component of the path is a file" do File::Info.writable?(datapath("dir", "test_file.txt", "")).should be_false end it "gives false when the file has no write permissions" do with_tempfile("readonly.txt") do |path| File.write(path, "") File.chmod(path, 0o444) pending_if_superuser! File::Info.writable?(path).should be_false end end it "follows symlinks" do with_tempfile("good_symlink_w.txt", "bad_symlink_w.txt", "readonly.txt") do |good_path, bad_path, readonly| File.write(readonly, "") File.chmod(readonly, 0o444) pending_if_superuser! File.symlink(File.expand_path(datapath("test_file.txt")), good_path) File.symlink(File.expand_path(readonly), bad_path) File::Info.writable?(good_path).should be_true File::Info.writable?(bad_path).should be_false end end it "gives false when the symbolic link destination doesn't exist" do with_tempfile("missing_symlink_w.txt") do |missing_path| File.symlink(File.expand_path(datapath("non_existing_file.txt")), missing_path) File::Info.writable?(missing_path).should be_false end end end end describe "size" do it { File.size(datapath("test_file.txt")).should eq(240) } it do File.open(datapath("test_file.txt"), "r") do |file| file.size.should eq(240) end end it "raises an error when the file does not exist" do filename = datapath("non_existing_file.txt") expect_raises(File::NotFoundError, "Unable to get file info: '#{filename.inspect_unquoted}'") do File.size(filename) end end # TODO: do we even want this? it "raises an error when a component of the path is a file" do expect_raises(File::Error, "Unable to get file info: '#{datapath("test_file.txt", "").inspect_unquoted}'") do File.size(datapath("test_file.txt", "")) end end end describe ".delete" do it "deletes a file" do with_tempfile("delete-file.txt") do |filename| File.open(filename, "w") { } File.exists?(filename).should be_true File.delete(filename) File.exists?(filename).should be_false end end it "deletes an open file" do with_tempfile("delete-file.txt") do |filename| file = File.open filename, "w" File.exists?(file.path).should be_true file.delete File.exists?(file.path).should be_false end end it "deletes a read-only file" do with_tempfile("delete-file-dir") do |path| Dir.mkdir(path) File.chmod(path, 0o755) filename = File.join(path, "foo") File.open(filename, "w") { } File.exists?(filename).should be_true File.chmod(filename, 0o000) File.delete(filename) File.exists?(filename).should be_false end end it "deletes? a file" do with_tempfile("delete-file.txt") do |filename| File.open(filename, "w") { } File.exists?(filename).should be_true File.delete?(filename).should be_true File.exists?(filename).should be_false File.delete?(filename).should be_false end end it "raises when file doesn't exist" do with_tempfile("nonexistent_file.txt") do |path| expect_raises(File::NotFoundError, "Error deleting file: '#{path.inspect_unquoted}'") do File.delete(path) end end end it "deletes a symlink directory" do with_tempfile("delete-target-directory", "delete-symlink-directory") do |target_path, symlink_path| Dir.mkdir(target_path) File.symlink(target_path, symlink_path) File.delete(symlink_path) end end end describe "rename" do it "renames a file" do with_tempfile("rename-source.txt", "rename-target.txt") do |source_path, target_path| File.write(source_path, "hello") File.rename(source_path, target_path) File.exists?(source_path).should be_false File.exists?(target_path).should be_true File.read(target_path).strip.should eq("hello") File.delete(target_path) end end it "replaces a file" do with_tempfile("rename-source.txt", "rename-target.txt") do |source_path, target_path| File.write(source_path, "foo") File.write(target_path, "bar") File.rename(source_path, target_path) File.exists?(source_path).should be_false File.read(target_path).strip.should eq("foo") File.delete(target_path) end end it "raises if old file doesn't exist" do with_tempfile("rename-fail-source.txt", "rename-fail-target.txt") do |source_path, target_path| expect_raises(File::NotFoundError, "Error renaming file: '#{source_path.inspect_unquoted}' -> '#{target_path.inspect_unquoted}'") do File.rename(source_path, target_path) end end end it "renames a File instance" do with_tempfile("rename-source.txt", "rename-target.txt") do |source_path, target_path| f = File.new(source_path, "w") f.rename target_path f.path.should eq target_path File.exists?(source_path).should be_false File.exists?(target_path).should be_true end end end # There are more detailed specs for `Path#expand` in path_spec.cr describe ".expand_path" do it "converts a pathname to an absolute pathname" do File.expand_path("a/b").should eq(Path.new("a/b").expand(Dir.current).to_s) File.expand_path("a/b", "c/d").should eq(Path.new("a/b").expand("c/d").to_s) File.expand_path("~/b", home: "c/d").should eq(Path.new("~/b").expand(Dir.current, home: "c/d").to_s) File.expand_path("~/b", "c/d", home: false).should eq(Path.new("~/b").expand("c/d", home: false).to_s) File.expand_path(Path.new("a/b")).should eq(Path.new("a/b").expand(Dir.current).to_s) end end describe "#realpath" do it "expands paths for normal files" do path = File.join(File.realpath("."), datapath("dir")) File.realpath(path).should eq(path) File.realpath(File.join(path, "..")).should eq(File.dirname(path)) end it "raises if file doesn't exist" do path = datapath("doesnotexist") expect_raises(File::NotFoundError, "Error resolving real path: '#{path.inspect_unquoted}'") do File.realpath(path) end end it "expands paths of symlinks" do file_path = File.expand_path(datapath("test_file.txt")) with_tempfile("symlink.txt") do |symlink_path| File.symlink(file_path, symlink_path) real_symlink_path = File.realpath(symlink_path) real_file_path = File.realpath(file_path) real_symlink_path.should eq(real_file_path) end end it "expands multiple layers of symlinks" do file_path = File.expand_path(datapath("test_file.txt")) with_tempfile("symlink1.txt") do |symlink_path1| with_tempfile("symlink2.txt") do |symlink_path2| File.symlink(file_path, symlink_path1) File.symlink(symlink_path1, symlink_path2) real_symlink_path = File.realpath(symlink_path2) real_file_path = File.realpath(file_path) real_symlink_path.should eq(real_file_path) end end end end describe "write" do it "can write to a file" do with_tempfile("write.txt") do |path| File.write(path, "hello") File.read(path).should eq("hello") end end it "writes bytes" do with_tempfile("write-bytes.txt") do |path| File.write(path, "hello".to_slice) File.read(path).should eq("hello") end end it "writes io" do with_tempfile("write-io.txt") do |path| File.open(datapath("test_file.txt")) do |file| File.write(path, file) end File.read(path).should eq(File.read(datapath("test_file.txt"))) end end it "raises if trying to write to a file not opened for writing" do with_tempfile("write-fails.txt") do |path| File.write(path, "hello") expect_raises(IO::Error, "File not open for writing") do File.open(path) { |file| file << "hello" } end end end it "can create a new file in append mode" do with_tempfile("append-create.txt") do |path| File.write(path, "hello", mode: "a") File.read(path).should eq("hello") end end it "can append to an existing file" do with_tempfile("append-existing.txt") do |path| File.write(path, "hello") File.read(path).should eq("hello") File.write(path, " world", mode: "a") File.read(path).should eq("hello world") end end end it "does to_s and inspect" do File.open(datapath("test_file.txt")) do |file| file.to_s.should eq("#") file.inspect.should eq("#") end end describe "close" do it "is not closed when opening" do File.open(datapath("test_file.txt")) do |file| file.closed?.should be_false end end it "is closed when closed" do file = File.new(datapath("test_file.txt")) file.close file.closed?.should be_true end it "should not raise when closing twice" do file = File.new(datapath("test_file.txt")) file.close file.close end it "does to_s when closed" do file = File.new(datapath("test_file.txt")) file.close file.to_s.should eq("#") file.inspect.should eq("#") end end it "supports the `b` mode flag" do with_tempfile("b-mode-flag.txt") do |path| File.open(path, "wb") do |f| f.write(Bytes[1, 3, 6, 10]) end File.open(path, "rb") do |f| bytes = Bytes.new(4) f.read(bytes) bytes.should eq(Bytes[1, 3, 6, 10]) end File.open(path, "ab") do |f| f.size.should eq(4) end File.open(path, "r+b") do |f| bytes = Bytes.new(4) f.read(bytes) bytes.should eq(Bytes[1, 3, 6, 10]) f.seek(0) f.write(Bytes[1, 3, 6, 10]) end File.open(path, "a+b") do |f| f.write(Bytes[13, 13, 10]) f.flush f.seek(0) bytes = Bytes.new(7) f.read(bytes) bytes.should eq(Bytes[1, 3, 6, 10, 13, 13, 10]) end File.open(path, "w+b") do |f| f.size.should eq(0) end File.open(path, "rb+") { } File.open(path, "wb+") { } File.open(path, "ab+") { } end end it "opens with perm (int)" do with_tempfile("write_with_perm-int.txt") do |path| perm = 0o600 File.open(path, "w", perm) do |file| file.info.permissions.should eq(normalize_permissions(perm, directory: false)) end end end it "opens with perm (File::Permissions)" do with_tempfile("write_with_perm.txt") do |path| perm = File::Permissions.flags(OwnerRead, OwnerWrite) File.open(path, "w", perm) do |file| file.info.permissions.should eq(normalize_permissions(perm.value, directory: false)) end end end it "clears the read buffer after a seek" do File.open(datapath("test_file.txt")) do |file| file.gets(5).should eq("Hello") file.seek(1) file.gets(4).should eq("ello") end end it "seeks from the current position" do File.open(datapath("test_file.txt")) do |file| file.gets(5) file.seek(-4, IO::Seek::Current) file.tell.should eq(1) end end it "raises if invoking seek with a closed file" do file = File.new(datapath("test_file.txt")) file.close expect_raises(IO::Error, "Closed stream") { file.seek(1) } end it "returns the current read position with tell" do File.open(datapath("test_file.txt")) do |file| file.tell.should eq(0) file.gets(5).should eq("Hello") file.tell.should eq(5) file.sync = true file.tell.should eq(5) end end it "returns the current write position with tell" do with_tempfile("delete-file.txt") do |filename| File.open(filename, "w") do |file| file.tell.should eq(0) file.write "12345".to_slice file.tell.should eq(5) file.sync = true file.tell.should eq(5) end end end it "returns the actual position with tell after append" do with_tempfile("delete-file.txt") do |filename| File.write(filename, "hello") File.open(filename, "a") do |file| file.write "12345".to_slice file.tell.should eq(10) end end end it "does not overwrite existing content in append mode" do with_tempfile("append-override.txt") do |filename| File.write(filename, "0123456789") File.open(filename, "a") do |file| file.seek(5) file.write "abcd".to_slice end File.read(filename).should eq "0123456789abcd" end end it "truncates file opened in append mode (#14702)" do with_tempfile("truncate-append.txt") do |path| File.write(path, "0123456789") File.open(path, "a") do |file| file.truncate(4) end File.read(path).should eq "0123" end end it "locks file opened in append mode (#14702)" do with_tempfile("truncate-append.txt") do |path| File.write(path, "0123456789") File.open(path, "a") do |file| file.flock_exclusive { } end end end it "can navigate with pos" do File.open(datapath("test_file.txt")) do |file| file.pos = 3 file.gets(2).should eq("lo") file.pos -= 4 file.gets(4).should eq("ello") end end it "raises if invoking tell with a closed file" do file = File.new(datapath("test_file.txt")) file.close expect_raises(IO::Error, "Closed stream") { file.tell } end it "iterates with each_char" do File.open(datapath("test_file.txt")) do |file| i = 0 file.each_char do |char| case i when 0 then char.should eq('H') when 1 then char.should eq('e') else break end i += 1 end end end it "iterates with each_byte" do File.open(datapath("test_file.txt")) do |file| i = 0 file.each_byte do |byte| case i when 0 then byte.should eq('H'.ord) when 1 then byte.should eq('e'.ord) else break end i += 1 end end end it "rewinds" do File.open(datapath("test_file.txt")) do |file| content = file.gets_to_end content.size.should_not eq(0) file.rewind file.gets_to_end.should eq(content) end end # Crystal does not expose ways to make a file unreadable on Windows {% unless flag?(:win32) %} it "raises when reading a file with no permission" do with_tempfile("file.txt") do |path| File.touch(path) File.chmod(path, File::Permissions::None) pending_if_superuser! expect_raises(File::AccessDeniedError, "Error opening file with mode 'r': '#{path.inspect_unquoted}'") { File.read(path) } end end {% end %} it "raises when writing to a file with no permission" do with_tempfile("file.txt") do |path| File.touch(path) File.chmod(path, File::Permissions::None) pending_if_superuser! expect_raises(File::AccessDeniedError, "Error opening file with mode 'w': '#{path.inspect_unquoted}'") { File.write(path, "foo") } end end describe "truncate" do it "truncates" do with_tempfile("truncate.txt") do |path| File.write(path, "0123456789") File.open(path, "r+") do |f| f.gets_to_end.should eq("0123456789") f.rewind f.puts("333") f.truncate(4) end File.read(path).should eq("333\n") end end it "truncates completely when no size is passed" do with_tempfile("truncate-no_size.txt") do |path| File.write(path, "0123456789") File.open(path, "r+") do |f| f.puts("333") f.truncate end File.read(path).should eq("") end end it "requires a file opened for writing" do with_tempfile("truncate-opened.txt") do |path| File.write(path, "0123456789") File.open(path, "r") do |f| expect_raises(File::Error, "Error truncating file: '#{path.inspect_unquoted}'") do f.truncate(4) end end end end end describe "fsync" do it "syncs OS file buffer to disk" do with_tempfile("fsync.txt") do |path| File.open(path, "a") do |f| f.puts("333") f.fsync File.read(path).should eq("333\n") end end end end describe "flock" do it "#flock_exclusive" do File.open(datapath("test_file.txt")) do |file1| File.open(datapath("test_file.txt")) do |file2| file1.flock_exclusive do exc = expect_raises(IO::Error, "Error applying file lock: file is already locked") do file2.flock_exclusive(blocking: false) { } end exc.os_error.should eq({% if flag?(:win32) %}WinError::ERROR_LOCK_VIOLATION{% else %}Errno::EWOULDBLOCK{% end %}) end end end end {true, false}.each do |blocking| context "blocking: #{blocking}" do it "#flock_shared" do File.open(datapath("test_file.txt"), blocking: blocking) do |file1| File.open(datapath("test_file.txt"), blocking: blocking) do |file2| file1.flock_shared do file2.flock_shared(blocking: false) { } end end end end it "#flock_shared soft blocking fiber" do File.open(datapath("test_file.txt"), blocking: blocking) do |file1| File.open(datapath("test_file.txt"), blocking: blocking) do |file2| done = Channel(Nil).new file1.flock_exclusive spawn do file1.flock_unlock done.send nil end file2.flock_shared done.receive end end end it "#flock_exclusive soft blocking fiber" do File.open(datapath("test_file.txt"), blocking: blocking) do |file1| File.open(datapath("test_file.txt"), blocking: blocking) do |file2| done = Channel(Nil).new file1.flock_exclusive spawn do file1.flock_unlock done.send nil end file2.flock_exclusive done.receive end end end end end end it "reads at offset" do filename = datapath("test_file.txt") {true, false}.each do |blocking| File.open(filename, blocking: blocking) do |file| file.read_at(6, 100) do |io| io.gets_to_end.should eq("World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello Worl") end file.read_at(0, 240) do |io| io.gets_to_end.should eq(File.read(filename)) end file.read_at(6_i64, 5_i64) do |io| io.gets_to_end.should eq("World") end end end end it "raises when reading at offset outside of bounds" do with_tempfile("read-out_of_bounds") do |path| File.write(path, "hello world") begin File.open(path) do |io| expect_raises(ArgumentError, "Negative bytesize") do io.read_at(3, -1) { } end expect_raises(ArgumentError, "Offset out of bounds") do io.read_at(12, 1) { } end expect_raises(ArgumentError, "Bytesize out of bounds") do io.read_at(6, 6) { } end end end end end describe "raises on null byte" do it_raises_on_null_byte "new" do File.new("foo\0bar") end it_raises_on_null_byte "join" do File.join("foo", "\0bar") end it_raises_on_null_byte "size" do File.size("foo\0bar") end it_raises_on_null_byte "rename (first arg)" do File.rename("foo\0bar", "baz") end it_raises_on_null_byte "rename (second arg)" do File.rename("baz", "foo\0bar") end it_raises_on_null_byte "info" do File.info("foo\0bar") end it_raises_on_null_byte "info?" do File.info?("foo\0bar") end it_raises_on_null_byte "exists?" do File.exists?("foo\0bar") end it_raises_on_null_byte "readable?" do File::Info.readable?("foo\0bar") end it_raises_on_null_byte "writable?" do File::Info.writable?("foo\0bar") end it_raises_on_null_byte "executable?" do File::Info.executable?("foo\0bar") end it_raises_on_null_byte "file?" do File.file?("foo\0bar") end it_raises_on_null_byte "directory?" do File.directory?("foo\0bar") end it_raises_on_null_byte "dirname" do File.dirname("foo\0bar") end it_raises_on_null_byte "basename" do File.basename("foo\0bar") end it_raises_on_null_byte "basename 2, first arg" do File.basename("foo\0bar", "baz") end it_raises_on_null_byte "basename 2, second arg" do File.basename("foobar", "baz\0") end it_raises_on_null_byte "delete" do File.delete("foo\0bar") end it_raises_on_null_byte "extname" do File.extname("foo\0bar") end it_raises_on_null_byte "expand_path, first arg" do File.expand_path("foo\0bar") end it_raises_on_null_byte "expand_path, second arg" do File.expand_path("baz", "foo\0bar") end it_raises_on_null_byte "link, first arg" do File.link("foo\0bar", "baz") end it_raises_on_null_byte "link, second arg" do File.link("baz", "foo\0bar") end it_raises_on_null_byte "symlink, first arg" do File.symlink("foo\0bar", "baz") end it_raises_on_null_byte "symlink, second arg" do File.symlink("baz", "foo\0bar") end it_raises_on_null_byte "symlink?" do File.symlink?("foo\0bar") end end describe "#delete" do it "deletes" do path = datapath("file-to-be-deleted") File.touch(path) file = File.new path file.close File.exists?(path).should be_true file.delete File.exists?(path).should be_false ensure File.delete(path) if path && File.exists?(path) end end {% unless flag?(:without_iconv) %} describe "encoding" do it "writes with encoding" do with_tempfile("encoding-write.txt") do |path| File.write(path, "hello", encoding: "UCS-2LE") File.read(path).to_slice.should eq("hello".encode("UCS-2LE")) end end it "reads with encoding" do with_tempfile("encoding-read.txt") do |path| File.write(path, "hello", encoding: "UCS-2LE") File.read(path, encoding: "UCS-2LE").should eq("hello") end end it "opens with encoding" do with_tempfile("encoding-open.txt") do |path| File.write(path, "hello", encoding: "UCS-2LE") File.open(path, encoding: "UCS-2LE") do |file| file.gets_to_end.should eq("hello") end end end it "does each line with encoding" do with_tempfile("encoding-each_line.txt") do |path| File.write(path, "hello", encoding: "UCS-2LE") File.each_line(path, encoding: "UCS-2LE") do |line| line.should eq("hello") end end end it "reads lines with encoding" do with_tempfile("encoding-read_lines.txt") do |path| File.write(path, "hello", encoding: "UCS-2LE") File.read_lines(path, encoding: "UCS-2LE").should eq(["hello"]) end end end {% end %} describe "closed stream" do it "raises if writing on a closed stream" do io = File.open(datapath("test_file.txt"), "r") io.close expect_raises(IO::Error, "Closed stream") { io.gets_to_end } expect_raises(IO::Error, "Closed stream") { io.print "hi" } expect_raises(IO::Error, "Closed stream") { io.puts "hi" } expect_raises(IO::Error, "Closed stream") { io.seek(1) } expect_raises(IO::Error, "Closed stream") { io.gets } expect_raises(IO::Error, "Closed stream") { io.read_byte } expect_raises(IO::Error, "Closed stream") { io.write_byte('a'.ord.to_u8) } end end describe "utime" do it "sets times with class method" do with_tempfile("utime-set.txt") do |path| File.write(path, "") atime = Time.utc(2000, 1, 2) mtime = Time.utc(2000, 3, 4) File.utime(atime, mtime, path) info = File.info(path) info.modification_time.should eq(mtime) end end it "sets times with instance method" do with_tempfile("utime-set.txt") do |path| File.open(path, "w") do |file| atime = Time.utc(2000, 1, 2) mtime = Time.utc(2000, 3, 4) file.utime(atime, mtime) info = File.info(path) info.modification_time.should eq(mtime) end end end it "raises if file not found" do atime = Time.utc(2000, 1, 2) mtime = Time.utc(2000, 3, 4) expect_raises(File::NotFoundError, "Error setting time on file: '#{datapath("nonexistent_file.txt").inspect_unquoted}'") do File.utime(atime, mtime, datapath("nonexistent_file.txt")) end end end describe ".touch" do it "creates file if it doesn't exist" do with_tempfile("touch-create.txt") do |path| File.exists?(path).should be_false File.touch(path) File.exists?(path).should be_true end end it "sets file times to given time" do time = Time.utc(2000, 3, 4) with_tempfile("touch-times.txt") do |path| File.touch(path, time) info = File.info(path) info.modification_time.should eq(time) end end it "sets file times to current time if no time argument given" do with_tempfile("touch-time_now.txt") do |path| File.touch(path) info = File.info(path) info.modification_time.should be_close(Time.utc, 1.second) end end it "raises if path contains non-existent directory" do with_tempfile(File.join("nonexistent-dir", "touch.txt")) do |path| expect_raises(File::NotFoundError, "Error opening file with mode 'a': '#{path.inspect_unquoted}'") do File.touch(path) end end end describe "touches existing" do it "file" do with_tempfile("touch-file") do |path| File.write(path, "") File.touch(path, Time.utc(2021, 1, 23)) info = File.info(path) info.modification_time.should eq Time.utc(2021, 1, 23) File.touch(path) info = File.info(path) info.modification_time.should be_close(Time.utc, 1.second) end end it "directory" do with_tempfile("touch-directory") do |path| Dir.mkdir(path) File.touch(path, Time.utc(2021, 1, 23)) info = File.info(path) info.modification_time.should eq Time.utc(2021, 1, 23) File.touch(path) info = File.info(path) info.modification_time.should be_close(Time.utc, 1.second) end end end it "raises if file cannot be accessed" do # This path is invalid because it represents a file path as a directory path path = File.join(datapath("test_file.txt"), "doesnotexist") expect_raises(File::Error, path.inspect_unquoted) do File.touch(path) end end end describe ".same_content?" do it "compares two equal files" do File.same_content?( datapath("test_file.txt"), datapath("test_file.txt") ).should be_true end it "compares two different files" do File.same_content?( datapath("test_file.txt"), datapath("test_file.ini") ).should be_false end end describe ".copy" do it "copies a file" do src_path = datapath("test_file.txt") with_tempfile("cp.txt") do |out_path| File.copy(src_path, out_path) File.exists?(out_path).should be_true File.same_content?(src_path, out_path).should be_true end end it "copies permissions" do with_tempfile("cp-permissions-src.txt", "cp-permissions-out.txt") do |src_path, out_path| File.write(src_path, "foo") File.chmod(src_path, 0o444) File.copy(src_path, out_path) File.info(out_path).permissions.should eq(File::Permissions.new(0o444)) File.same_content?(src_path, out_path).should be_true end end it "overwrites existing destination and permissions" do with_tempfile("cp-permissions-src.txt", "cp-permissions-out.txt") do |src_path, out_path| File.write(src_path, "foo") File.chmod(src_path, 0o444) File.write(out_path, "bar") File.chmod(out_path, 0o666) File.copy(src_path, out_path) File.info(out_path).permissions.should eq(File::Permissions.new(0o444)) File.same_content?(src_path, out_path).should be_true end end it "copies read-only permission" do with_tempfile("cp-permissions-src.txt", "cp-permissions-out.txt") do |src_path, out_path| File.write(src_path, "foo") File.chmod(src_path, 0o444) File.copy(src_path, out_path) File.info(out_path).permissions.should eq normalize_permissions(0o444, directory: false) File.same_content?(src_path, out_path).should be_true end end it "copies read-only permission over existing file" do with_tempfile("cp-permissions-src.txt", "cp-permissions-out.txt") do |src_path, out_path| File.write(src_path, "foo") File.chmod(src_path, 0o444) File.write(out_path, "bar") File.copy(src_path, out_path) File.info(out_path).permissions.should eq normalize_permissions(0o444, directory: false) File.same_content?(src_path, out_path).should be_true end end end describe File::Permissions do it "does to_s" do perm = File::Permissions.flags(OwnerAll, GroupRead, GroupWrite, OtherRead) perm.to_s.should eq("rwxrw-r-- (0o764)") perm.inspect.should eq("File::Permissions[OtherRead, GroupWrite, GroupRead, OwnerExecute, OwnerWrite, OwnerRead]") perm.pretty_inspect.should eq("File::Permissions[OtherRead, GroupWrite, GroupRead, OwnerExecute, OwnerWrite, OwnerRead]") end end end