diff --git a/FileOtter/File.swift b/FileOtter/File.swift index 56f00f8..66a9f1d 100644 --- a/FileOtter/File.swift +++ b/FileOtter/File.swift @@ -320,7 +320,8 @@ public extension File { } static func isEmpty(_ url: URL) throws -> Bool { - fatalError("Not implemented") + let size = try self.size(url) + return size == 0 } // Ruby aliases @@ -397,15 +398,16 @@ public extension File { } static func symlink(source: URL, destination: URL) throws { - fatalError("Not implemented") + try FileManager.default.createSymbolicLink(at: destination, withDestinationURL: source) } static func readlink(_ url: URL) throws -> URL { - fatalError("Not implemented") + let path = try FileManager.default.destinationOfSymbolicLink(atPath: url.path) + return URL(fileURLWithPath: path) } static func unlink(_ url: URL) throws { - fatalError("Not implemented") + try FileManager.default.removeItem(at: url) } static func delete(_ url: URL) throws { @@ -413,7 +415,11 @@ public extension File { } static func rename(source: URL, destination: URL) throws { - fatalError("Not implemented") + // Use replaceItem - it works whether destination exists or not + // and provides atomic replacement when it does exist + _ = try FileManager.default.replaceItem( + at: destination, withItemAt: source, backupItemName: nil, resultingItemURL: nil + ) } static func truncate(_ url: URL, to size: Int) throws { @@ -421,7 +427,15 @@ public extension File { } static func touch(_ url: URL) throws { - fatalError("Not implemented") + let fm = FileManager.default + + if fm.fileExists(atPath: url.path) { + // Update modification time to current time + try fm.setAttributes([.modificationDate: Date()], ofItemAtPath: url.path) + } else { + // Create empty file + fm.createFile(atPath: url.path, contents: nil, attributes: nil) + } } static func utime(_ url: URL, atime: Date, mtime: Date) throws { diff --git a/FileOtterTests/FileOtterTests.swift b/FileOtterTests/DirTests.swift similarity index 100% rename from FileOtterTests/FileOtterTests.swift rename to FileOtterTests/DirTests.swift diff --git a/FileOtterTests/FileOperationTests.swift b/FileOtterTests/FileOperationTests.swift index 38b1576..64d7fef 100644 --- a/FileOtterTests/FileOperationTests.swift +++ b/FileOtterTests/FileOperationTests.swift @@ -43,53 +43,129 @@ final class FileOperationTests: XCTestCase { } func testSymlink() throws { - // TODO: Implement - // File.symlink(source, destination) creates symbolic link + // 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 { - // TODO: Implement + 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 { - // TODO: Implement - // File.readlink(url) returns target of symlink + // 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 { - // TODO: Implement + // Regular file, not a symlink + XCTAssertThrowsError(try File.readlink(sourceFile)) } // MARK: - Delete Tests func testUnlink() throws { - // TODO: Implement - // File.unlink(url) deletes file + // 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 { - // TODO: Implement + let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt") + XCTAssertThrowsError(try File.unlink(nonExistent)) } func testDelete() throws { - // TODO: Implement - // File.delete(url) is alias for unlink + // 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 { - // TODO: Implement - // File.rename(source, destination) moves/renames file + // 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 { - // TODO: Implement - // rename should overwrite if dest exists + // 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 testRenameThrowsForNonExistent() throws { - // TODO: Implement + 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 @@ -116,13 +192,31 @@ final class FileOperationTests: XCTestCase { // MARK: - Touch Tests func testTouch() throws { - // TODO: Implement - // File.touch(url) updates access/modification times + // 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 { - // TODO: Implement - // touch creates file if it doesn't exist + // 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 diff --git a/FileOtterTests/FileTypeTests.swift b/FileOtterTests/FileTypeTests.swift index 1f40fcf..f10f23e 100644 --- a/FileOtterTests/FileTypeTests.swift +++ b/FileOtterTests/FileTypeTests.swift @@ -133,22 +133,35 @@ final class FileTypeTests: XCTestCase { // MARK: - Empty/Zero Tests func testIsEmpty() throws { - // TODO: Implement - // File.isEmpty(url) returns true for empty file + // Create empty file + let emptyFile = tempDir.appendingPathComponent("empty.txt") + try "".write(to: emptyFile, atomically: true, encoding: .utf8) + XCTAssertTrue(try File.isEmpty(emptyFile)) } func testIsEmptyForNonEmpty() throws { - // TODO: Implement - // File.isEmpty(url) returns false for non-empty file + // testFile has content + XCTAssertFalse(try File.isEmpty(testFile)) + + // Create another non-empty file + let nonEmptyFile = tempDir.appendingPathComponent("nonempty.txt") + try "Some content".write(to: nonEmptyFile, atomically: true, encoding: .utf8) + XCTAssertFalse(try File.isEmpty(nonEmptyFile)) } func testIsEmptyThrowsForNonExistent() throws { - // TODO: Implement + let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt") + XCTAssertThrowsError(try File.isEmpty(nonExistent)) } func testIsZero() throws { - // TODO: Implement - // File.isZero(url) is alias for isEmpty + // Create empty file + let emptyFile = tempDir.appendingPathComponent("zero.txt") + try "".write(to: emptyFile, atomically: true, encoding: .utf8) + + // isZero is alias for isEmpty + XCTAssertTrue(try File.isZero(emptyFile)) + XCTAssertFalse(try File.isZero(testFile)) } // MARK: - ftype Tests