mirror of
https://github.com/samsonjs/FileOtter.git
synced 2026-04-27 14:57:41 +00:00
WIP: Implement more of File
This commit is contained in:
parent
e59a68901a
commit
ab21045498
4 changed files with 155 additions and 34 deletions
|
|
@ -320,7 +320,8 @@ public extension File {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isEmpty(_ url: URL) throws -> Bool {
|
static func isEmpty(_ url: URL) throws -> Bool {
|
||||||
fatalError("Not implemented")
|
let size = try self.size(url)
|
||||||
|
return size == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ruby aliases
|
// Ruby aliases
|
||||||
|
|
@ -397,15 +398,16 @@ public extension File {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func symlink(source: URL, destination: URL) throws {
|
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 {
|
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 {
|
static func unlink(_ url: URL) throws {
|
||||||
fatalError("Not implemented")
|
try FileManager.default.removeItem(at: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func delete(_ url: URL) throws {
|
static func delete(_ url: URL) throws {
|
||||||
|
|
@ -413,7 +415,11 @@ public extension File {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func rename(source: URL, destination: URL) throws {
|
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 {
|
static func truncate(_ url: URL, to size: Int) throws {
|
||||||
|
|
@ -421,7 +427,15 @@ public extension File {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func touch(_ url: URL) throws {
|
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 {
|
static func utime(_ url: URL, atime: Date, mtime: Date) throws {
|
||||||
|
|
|
||||||
|
|
@ -43,53 +43,129 @@ final class FileOperationTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSymlink() throws {
|
func testSymlink() throws {
|
||||||
// TODO: Implement
|
// Create a file to link to
|
||||||
// File.symlink(source, destination) creates symbolic link
|
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 {
|
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 {
|
func testReadlink() throws {
|
||||||
// TODO: Implement
|
// Create target and symlink
|
||||||
// File.readlink(url) returns target of 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 {
|
func testReadlinkThrowsForNonSymlink() throws {
|
||||||
// TODO: Implement
|
// Regular file, not a symlink
|
||||||
|
XCTAssertThrowsError(try File.readlink(sourceFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delete Tests
|
// MARK: - Delete Tests
|
||||||
|
|
||||||
func testUnlink() throws {
|
func testUnlink() throws {
|
||||||
// TODO: Implement
|
// Create a file to delete
|
||||||
// File.unlink(url) deletes file
|
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 {
|
func testUnlinkThrowsForNonExistent() throws {
|
||||||
// TODO: Implement
|
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
|
||||||
|
XCTAssertThrowsError(try File.unlink(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDelete() throws {
|
func testDelete() throws {
|
||||||
// TODO: Implement
|
// Create a file to delete
|
||||||
// File.delete(url) is alias for unlink
|
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
|
// MARK: - Rename Tests
|
||||||
|
|
||||||
func testRename() throws {
|
func testRename() throws {
|
||||||
// TODO: Implement
|
// Create source file
|
||||||
// File.rename(source, destination) moves/renames 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 {
|
func testRenameOverwritesExisting() throws {
|
||||||
// TODO: Implement
|
// Create source and destination files
|
||||||
// rename should overwrite if dest exists
|
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 {
|
func testRenameThrowsForNonExistentSource() throws {
|
||||||
// TODO: Implement
|
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
|
// MARK: - Truncate Tests
|
||||||
|
|
@ -116,13 +192,31 @@ final class FileOperationTests: XCTestCase {
|
||||||
// MARK: - Touch Tests
|
// MARK: - Touch Tests
|
||||||
|
|
||||||
func testTouch() throws {
|
func testTouch() throws {
|
||||||
// TODO: Implement
|
// Create a file with old times
|
||||||
// File.touch(url) updates access/modification 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 {
|
func testTouchCreatesFile() throws {
|
||||||
// TODO: Implement
|
// Touch non-existent file should create it
|
||||||
// touch creates file if it doesn't exist
|
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
|
// MARK: - utime Tests
|
||||||
|
|
|
||||||
|
|
@ -133,22 +133,35 @@ final class FileTypeTests: XCTestCase {
|
||||||
// MARK: - Empty/Zero Tests
|
// MARK: - Empty/Zero Tests
|
||||||
|
|
||||||
func testIsEmpty() throws {
|
func testIsEmpty() throws {
|
||||||
// TODO: Implement
|
// Create empty file
|
||||||
// File.isEmpty(url) returns true for empty file
|
let emptyFile = tempDir.appendingPathComponent("empty.txt")
|
||||||
|
try "".write(to: emptyFile, atomically: true, encoding: .utf8)
|
||||||
|
XCTAssertTrue(try File.isEmpty(emptyFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsEmptyForNonEmpty() throws {
|
func testIsEmptyForNonEmpty() throws {
|
||||||
// TODO: Implement
|
// testFile has content
|
||||||
// File.isEmpty(url) returns false for non-empty file
|
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 {
|
func testIsEmptyThrowsForNonExistent() throws {
|
||||||
// TODO: Implement
|
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
|
||||||
|
XCTAssertThrowsError(try File.isEmpty(nonExistent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsZero() throws {
|
func testIsZero() throws {
|
||||||
// TODO: Implement
|
// Create empty file
|
||||||
// File.isZero(url) is alias for isEmpty
|
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
|
// MARK: - ftype Tests
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue