FileOtter/Tests/FileOtterTests/FileOperationTests.swift

417 lines
16 KiB
Swift

//
// FileOperationTests.swift
// FileOtterTests
//
// Created by Sami Samhuri on 2025-08-19.
//
@testable import FileOtter
import XCTest
final class FileOperationTests: XCTestCase {
var tempDir: URL!
var sourceFile: URL!
var destFile: URL!
override func setUpWithError() throws {
tempDir = URL.temporaryDirectory
.appendingPathComponent("FileOperationTests-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
sourceFile = tempDir.appendingPathComponent("source.txt")
try "Source content".write(to: sourceFile, atomically: true, encoding: .utf8)
destFile = tempDir.appendingPathComponent("dest.txt")
}
override func tearDownWithError() throws {
if FileManager.default.fileExists(atPath: tempDir.path) {
try FileManager.default.removeItem(at: tempDir)
}
}
// MARK: - Link Tests
func testLink() throws {
// Create hard link
let hardLink = tempDir.appendingPathComponent("hardlink.txt")
try File.link(source: sourceFile, destination: hardLink)
// Both files should exist
XCTAssertTrue(FileManager.default.fileExists(atPath: sourceFile.path))
XCTAssertTrue(FileManager.default.fileExists(atPath: hardLink.path))
// They should have the same content
XCTAssertEqual(try String(contentsOf: hardLink), "Source content")
// They should be the same file (same inode)
XCTAssertTrue(try File.identical(sourceFile, hardLink))
}
func testLinkThrowsIfDestExists() throws {
try "Existing content".write(to: destFile, atomically: true, encoding: .utf8)
// Should throw because destination exists
XCTAssertThrowsError(try File.link(source: sourceFile, destination: destFile))
// Destination should still have original content
XCTAssertEqual(try String(contentsOf: destFile), "Existing content")
}
func testSymlink() throws {
// Create a file to link to
let target = tempDir.appendingPathComponent("target.txt")
try "Target content".write(to: target, atomically: true, encoding: .utf8)
// Create symlink
let link = tempDir.appendingPathComponent("symlink.txt")
try File.symlink(source: target, destination: link)
// Verify symlink exists and points to target
XCTAssertTrue(FileManager.default.fileExists(atPath: link.path))
// Reading through symlink should get target's content
XCTAssertEqual(try String(contentsOf: link), "Target content")
}
func testSymlinkThrowsIfDestExists() throws {
let target = tempDir.appendingPathComponent("target.txt")
try "Target".write(to: target, atomically: true, encoding: .utf8)
let existingFile = tempDir.appendingPathComponent("existing.txt")
try "Existing".write(to: existingFile, atomically: true, encoding: .utf8)
// Should throw because destination exists
XCTAssertThrowsError(try File.symlink(source: target, destination: existingFile))
}
func testReadlink() throws {
// Create target and symlink
let target = tempDir.appendingPathComponent("readlink-target.txt")
try "Content".write(to: target, atomically: true, encoding: .utf8)
let link = tempDir.appendingPathComponent("readlink-symlink.txt")
try File.symlink(source: target, destination: link)
// readlink should return the target path
let readTarget = try File.readlink(link)
XCTAssertEqual(readTarget.lastPathComponent, "readlink-target.txt")
}
func testReadlinkThrowsForNonSymlink() throws {
// Regular file, not a symlink
XCTAssertThrowsError(try File.readlink(sourceFile))
}
// MARK: - Delete Tests
func testUnlink() throws {
// Create a file to delete
let fileToDelete = tempDir.appendingPathComponent("delete-me.txt")
try "Delete this file".write(to: fileToDelete, atomically: true, encoding: .utf8)
XCTAssertTrue(FileManager.default.fileExists(atPath: fileToDelete.path))
// Delete it
try File.unlink(fileToDelete)
XCTAssertFalse(FileManager.default.fileExists(atPath: fileToDelete.path))
}
func testUnlinkThrowsForNonExistent() throws {
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
XCTAssertThrowsError(try File.unlink(nonExistent))
}
func testDelete() throws {
// Create a file to delete
let fileToDelete = tempDir.appendingPathComponent("delete-me-too.txt")
try "Delete this too".write(to: fileToDelete, atomically: true, encoding: .utf8)
XCTAssertTrue(FileManager.default.fileExists(atPath: fileToDelete.path))
// Delete is an alias for unlink
try File.delete(fileToDelete)
XCTAssertFalse(FileManager.default.fileExists(atPath: fileToDelete.path))
}
// MARK: - Rename Tests
func testRename() throws {
// Create source file
let source = tempDir.appendingPathComponent("original.txt")
let dest = tempDir.appendingPathComponent("renamed.txt")
let content = "Original content"
try content.write(to: source, atomically: true, encoding: .utf8)
// Rename it
try File.rename(source: source, destination: dest)
// Source should not exist, dest should exist with same content
XCTAssertFalse(FileManager.default.fileExists(atPath: source.path))
XCTAssertTrue(FileManager.default.fileExists(atPath: dest.path))
XCTAssertEqual(try String(contentsOf: dest), content)
}
func testRenameOverwritesExisting() throws {
// Create source and destination files
let source = tempDir.appendingPathComponent("source.txt")
let dest = tempDir.appendingPathComponent("existing.txt")
try "Source content".write(to: source, atomically: true, encoding: .utf8)
try "Old content".write(to: dest, atomically: true, encoding: .utf8)
// Rename should overwrite destination
try File.rename(source: source, destination: dest)
XCTAssertFalse(FileManager.default.fileExists(atPath: source.path))
XCTAssertEqual(try String(contentsOf: dest), "Source content")
}
func testRenameThrowsForNonExistentSource() throws {
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
// Test 1: When destination doesn't exist
let newDest = tempDir.appendingPathComponent("new-dest.txt")
XCTAssertThrowsError(try File.rename(source: nonExistent, destination: newDest))
XCTAssertFalse(FileManager.default.fileExists(atPath: newDest.path))
// Test 2: When destination exists (should be preserved on failure)
let existingDest = tempDir.appendingPathComponent("existing.txt")
let importantContent = "Important data that must not be lost"
try importantContent.write(to: existingDest, atomically: true, encoding: .utf8)
XCTAssertThrowsError(try File.rename(source: nonExistent, destination: existingDest))
// Destination should still exist with original content
XCTAssertTrue(FileManager.default.fileExists(atPath: existingDest.path))
XCTAssertEqual(try String(contentsOf: existingDest), importantContent)
}
// MARK: - Truncate Tests
func testTruncate() throws {
// Create file with content
let file = tempDir.appendingPathComponent("truncate-test.txt")
let originalContent = "This is a longer piece of content that will be truncated"
try originalContent.write(to: file, atomically: true, encoding: .utf8)
// Truncate to 10 bytes
try File.truncate(file, to: 10)
// File should be truncated
XCTAssertEqual(try File.size(file), 10)
XCTAssertEqual(try String(contentsOf: file), "This is a ")
}
func testTruncateExpands() throws {
// Create small file
let file = tempDir.appendingPathComponent("expand-test.txt")
try "Short".write(to: file, atomically: true, encoding: .utf8)
// Expand to 20 bytes (should pad with zeros)
try File.truncate(file, to: 20)
XCTAssertEqual(try File.size(file), 20)
// Read raw data to check padding
let data = try Data(contentsOf: file)
XCTAssertEqual(data.count, 20)
// First 5 bytes should be "Short"
let shortData = "Short".data(using: .utf8)!
XCTAssertEqual(data.prefix(5), shortData)
// Remaining bytes should be zeros
for i in 5..<20 {
XCTAssertEqual(data[i], 0)
}
}
func testTruncateThrowsForNonExistent() throws {
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
XCTAssertThrowsError(try File.truncate(nonExistent, to: 10))
}
func testInstanceTruncate() throws {
// Skip for now as it requires File instance implementation
throw XCTSkip("Instance methods not yet implemented")
}
// MARK: - Touch Tests
func testTouch() throws {
// Create a file with old times
let file = tempDir.appendingPathComponent("touch-test.txt")
try "content".write(to: file, atomically: true, encoding: .utf8)
// Get initial mtime
let initialMtime = try File.mtime(file)
// Wait a moment and touch
Thread.sleep(forTimeInterval: 0.1)
try File.touch(file)
// mtime should be updated
let newMtime = try File.mtime(file)
XCTAssertGreaterThan(newMtime, initialMtime)
}
func testTouchCreatesFile() throws {
// Touch non-existent file should create it
let newFile = tempDir.appendingPathComponent("created-by-touch.txt")
XCTAssertFalse(FileManager.default.fileExists(atPath: newFile.path))
try File.touch(newFile)
XCTAssertTrue(FileManager.default.fileExists(atPath: newFile.path))
XCTAssertEqual(try File.size(newFile), 0) // Should be empty
}
// MARK: - utime Tests
func testUtime() throws {
// Create file
let file = tempDir.appendingPathComponent("utime-test.txt")
try "content".write(to: file, atomically: true, encoding: .utf8)
// Set specific times
let atime = Date(timeIntervalSince1970: 1000000)
let mtime = Date(timeIntervalSince1970: 2000000)
try File.utime(file, atime: atime, mtime: mtime)
// Verify times were set
let newMtime = try File.mtime(file)
XCTAssertEqual(newMtime.timeIntervalSince1970, mtime.timeIntervalSince1970, accuracy: 1.0)
}
func testUtimeFollowsSymlinks() throws {
// Create target and symlink
let target = tempDir.appendingPathComponent("utime-target.txt")
try "content".write(to: target, atomically: true, encoding: .utf8)
let link = tempDir.appendingPathComponent("utime-link.txt")
try File.symlink(source: target, destination: link)
// Set times via symlink
let atime = Date(timeIntervalSince1970: 1000000)
let mtime = Date(timeIntervalSince1970: 2000000)
try File.utime(link, atime: atime, mtime: mtime)
// Target should have new times
let targetMtime = try File.mtime(target)
XCTAssertEqual(targetMtime.timeIntervalSince1970, mtime.timeIntervalSince1970, accuracy: 1.0)
}
func testLutime() throws {
// Create target and symlink
let target = tempDir.appendingPathComponent("lutime-target.txt")
try "content".write(to: target, atomically: true, encoding: .utf8)
let link = tempDir.appendingPathComponent("lutime-link.txt")
try File.symlink(source: target, destination: link)
// Get original target mtime
let originalTargetMtime = try File.mtime(target)
// Set times on symlink itself (not target)
let atime = Date(timeIntervalSince1970: 1000000)
let mtime = Date(timeIntervalSince1970: 2000000)
try File.lutime(link, atime: atime, mtime: mtime)
// Target should still have original time
let targetMtime = try File.mtime(target)
XCTAssertEqual(targetMtime.timeIntervalSince1970, originalTargetMtime.timeIntervalSince1970, accuracy: 1.0)
// Symlink should have new time (check with lstat)
let linkStat = try File.linkStatus(link)
XCTAssertEqual(linkStat.mtime.timeIntervalSince1970, mtime.timeIntervalSince1970, accuracy: 1.0)
}
// MARK: - mkfifo Tests
func testMkfifo() throws {
// Create named pipe
let fifo = tempDir.appendingPathComponent("test.fifo")
try File.mkfifo(fifo)
// Verify it's a pipe
XCTAssertTrue(File.isPipe(fifo))
XCTAssertFalse(File.isFile(fifo))
XCTAssertFalse(File.isDirectory(fifo))
}
func testMkfifoWithPermissions() throws {
// Create named pipe with specific permissions
let fifo = tempDir.appendingPathComponent("test-perms.fifo")
try File.mkfifo(fifo, permissions: 0o644)
// Verify it's a pipe
XCTAssertTrue(File.isPipe(fifo))
// Verify permissions (masking out file type bits)
let stat = try File.fileStatus(fifo)
let perms = stat.mode & 0o777
// Actual permissions may be affected by umask
XCTAssertTrue(perms <= 0o644)
}
func testMkfifoThrowsIfExists() throws {
// Create a regular file
let file = tempDir.appendingPathComponent("existing.txt")
try "content".write(to: file, atomically: true, encoding: .utf8)
// Should throw when trying to create fifo with same name
XCTAssertThrowsError(try File.mkfifo(file))
// Original file should still be a regular file
XCTAssertTrue(File.isFile(file))
XCTAssertFalse(File.isPipe(file))
}
// MARK: - identical Tests
func testIdentical() throws {
// Same file should be identical to itself
XCTAssertTrue(try File.identical(sourceFile, sourceFile))
}
func testIdenticalForHardLink() throws {
// Create hard link
let hardLink = tempDir.appendingPathComponent("hardlink.txt")
try File.link(source: sourceFile, destination: hardLink)
// Hard links point to same inode
XCTAssertTrue(try File.identical(sourceFile, hardLink))
XCTAssertTrue(try File.identical(hardLink, sourceFile))
}
func testIdenticalForSymlink() throws {
// Create symlink
let symlink = tempDir.appendingPathComponent("symlink.txt")
try File.symlink(source: sourceFile, destination: symlink)
// Symlink and target should be identical (stat follows symlinks)
XCTAssertTrue(try File.identical(sourceFile, symlink))
XCTAssertTrue(try File.identical(symlink, sourceFile))
}
func testIdenticalForDifferent() throws {
// Create another file
let otherFile = tempDir.appendingPathComponent("other.txt")
try "Other content".write(to: otherFile, atomically: true, encoding: .utf8)
// Different files should not be identical
XCTAssertFalse(try File.identical(sourceFile, otherFile))
XCTAssertFalse(try File.identical(otherFile, sourceFile))
}
func testIdenticalForSameContent() throws {
// Create another file with same content
let copyFile = tempDir.appendingPathComponent("copy.txt")
try "Source content".write(to: copyFile, atomically: true, encoding: .utf8)
// Files with same content but different inodes are not identical
XCTAssertFalse(try File.identical(sourceFile, copyFile))
XCTAssertFalse(try File.identical(copyFile, sourceFile))
}
}