From f24ed714e6f52dff1bdb55ee7e2f7ce0a8d8dd22 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Mon, 25 Aug 2025 08:59:50 -0700 Subject: [PATCH] WIP: Implement File --- FileOtter/File.swift | 453 ++++++++++++++++++++++- FileOtter/Glob.swift | 2 +- FileOtterTests/FileFnmatchTests.swift | 129 +++++++ FileOtterTests/FileInfoTests.swift | 143 +++++++ FileOtterTests/FileOpenTests.swift | 151 ++++++++ FileOtterTests/FileOperationTests.swift | 187 ++++++++++ FileOtterTests/FilePathTests.swift | 167 +++++++++ FileOtterTests/FilePermissionTests.swift | 166 +++++++++ FileOtterTests/FileTypeTests.swift | 195 ++++++++++ 9 files changed, 1590 insertions(+), 3 deletions(-) create mode 100644 FileOtterTests/FileFnmatchTests.swift create mode 100644 FileOtterTests/FileInfoTests.swift create mode 100644 FileOtterTests/FileOpenTests.swift create mode 100644 FileOtterTests/FileOperationTests.swift create mode 100644 FileOtterTests/FilePathTests.swift create mode 100644 FileOtterTests/FilePermissionTests.swift create mode 100644 FileOtterTests/FileTypeTests.swift diff --git a/FileOtter/File.swift b/FileOtter/File.swift index 8fbccad..b8bf2e7 100644 --- a/FileOtter/File.swift +++ b/FileOtter/File.swift @@ -2,9 +2,458 @@ // File.swift // FileOtter // -// Created by Sami Samhuri on 2024-04-24. +// Created by Sami Samhuri on 2025-08-19. // import Foundation -public struct File {} +// MARK: - File Class + +/// A File object represents an open file with automatic resource management. +/// The file handle is automatically closed when the File object is deallocated. +public class File: CustomStringConvertible, CustomDebugStringConvertible { + private let handle: FileHandle + public let url: URL + public let mode: Mode + + // MARK: - Mode + + public enum Mode { + case read // r + case write // w + case append // a + case readWrite // r+ + case readWriteNew // w+ + case readAppend // a+ + case writeExclusive // wx (create, fail if exists) + } + + // MARK: - Initialization + + public init(url: URL, mode: Mode = .read, permissions: Int = 0o666) throws { + self.url = url + self.mode = mode + self.handle = FileHandle() // TODO: Implement proper opening + fatalError("Not implemented") + } + + deinit { + try? handle.close() + } + + // MARK: - Opening with blocks + + public static func open(url: URL, mode: Mode = .read, permissions: Int = 0o666) throws -> File { + fatalError("Not implemented") + } + + @discardableResult + public static func open(url: URL, mode: Mode = .read, permissions: Int = 0o666, _ block: (File) throws -> T) rethrows -> T { + fatalError("Not implemented") + } + + // MARK: - Instance Properties + + public var atime: Date { + fatalError("Not implemented") + } + + public var mtime: Date { + fatalError("Not implemented") + } + + public var ctime: Date { + fatalError("Not implemented") + } + + public var birthtime: Date { + fatalError("Not implemented") + } + + public var size: Int { + fatalError("Not implemented") + } + + // MARK: - Instance Methods + + public func chmod(_ permissions: Int) throws { + fatalError("Not implemented") + } + + public func chown(owner: Int? = nil, group: Int? = nil) throws { + fatalError("Not implemented") + } + + public func truncate(to size: Int) throws { + fatalError("Not implemented") + } + + public func flock(_ operation: LockOperation) throws { + fatalError("Not implemented") + } + + public func stat() throws -> FileStat { + fatalError("Not implemented") + } + + public func lstat() throws -> FileStat { + fatalError("Not implemented") + } + + public func close() throws { + fatalError("Not implemented") + } + + // MARK: - CustomStringConvertible + + public var description: String { + url.path + } + + public var debugDescription: String { + "" + } +} + +// MARK: - Static Path Operations + +public extension File { + static func basename(_ url: URL, suffix: String? = nil) -> String { + // Handle root path special case + if url.path == "/" { + return "/" + } + + // Get the last path component using URL's built-in method + let base = url.lastPathComponent + + // If no suffix specified, return the base + guard let suffix = suffix else { + return base + } + + // Handle wildcard suffix ".*" + if suffix == ".*" { + // Use URL's pathExtension to remove any extension + let withoutExt = url.deletingPathExtension().lastPathComponent + return withoutExt + } + + // Handle regular suffix + if base.hasSuffix(suffix) { + return String(base.dropLast(suffix.count)) + } + + return base + } + + static func dirname(_ url: URL, level: Int = 1) -> URL { + var result = url + for _ in 0.. String { + let ext = url.pathExtension + return ext.isEmpty ? "" : ".\(ext)" + } + + static func split(_ url: URL) -> (dir: URL, name: String) { + fatalError("Not implemented") + } + + static func join(_ components: String...) -> URL { + fatalError("Not implemented") + } + + static func join(_ components: [String]) -> URL { + fatalError("Not implemented") + } + + static func absolutePath(_ url: URL, relativeTo base: URL? = nil) -> URL { + fatalError("Not implemented") + } + + static func expandPath(_ path: String) -> URL { + fatalError("Not implemented") + } + + static func realpath(_ url: URL) throws -> URL { + fatalError("Not implemented") + } + + static func realdirpath(_ url: URL) throws -> URL { + fatalError("Not implemented") + } +} + +// MARK: - Static File Info + +public extension File { + static func atime(_ url: URL) throws -> Date { + fatalError("Not implemented") + } + + static func mtime(_ url: URL) throws -> Date { + fatalError("Not implemented") + } + + static func ctime(_ url: URL) throws -> Date { + fatalError("Not implemented") + } + + static func birthtime(_ url: URL) throws -> Date { + fatalError("Not implemented") + } + + static func size(_ url: URL) throws -> Int { + fatalError("Not implemented") + } + + static func stat(_ url: URL) throws -> FileStat { + fatalError("Not implemented") + } + + static func lstat(_ url: URL) throws -> FileStat { + fatalError("Not implemented") + } +} + +// MARK: - Static File Type Checks + +public extension File { + static func exists(_ url: URL) -> Bool { + FileManager.default.fileExists(atPath: url.path) + } + + static func isFile(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return exists && !isDirectory.boolValue + } + + static func isDirectory(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return exists && isDirectory.boolValue + } + + static func isSymlink(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isBlockDevice(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isCharDevice(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isPipe(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isSocket(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isEmpty(_ url: URL) throws -> Bool { + fatalError("Not implemented") + } + + // Ruby aliases + static func isZero(_ url: URL) throws -> Bool { + try isEmpty(url) + } +} + +// MARK: - Static Permission Checks + +public extension File { + static func isReadable(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isWritable(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isExecutable(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isOwned(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isGroupOwned(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isWorldReadable(_ url: URL) -> Int? { + fatalError("Not implemented") + } + + static func isWorldWritable(_ url: URL) -> Int? { + fatalError("Not implemented") + } + + static func isSetuid(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isSetgid(_ url: URL) -> Bool { + fatalError("Not implemented") + } + + static func isSticky(_ url: URL) -> Bool { + fatalError("Not implemented") + } +} + +// MARK: - Static File Operations + +public extension File { + static func chmod(_ url: URL, permissions: Int) throws { + fatalError("Not implemented") + } + + static func chown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws { + fatalError("Not implemented") + } + + static func lchmod(_ url: URL, permissions: Int) throws { + fatalError("Not implemented") + } + + static func lchown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws { + fatalError("Not implemented") + } + + static func link(source: URL, destination: URL) throws { + fatalError("Not implemented") + } + + static func symlink(source: URL, destination: URL) throws { + fatalError("Not implemented") + } + + static func readlink(_ url: URL) throws -> URL { + fatalError("Not implemented") + } + + static func unlink(_ url: URL) throws { + fatalError("Not implemented") + } + + static func delete(_ url: URL) throws { + try unlink(url) + } + + static func rename(source: URL, destination: URL) throws { + fatalError("Not implemented") + } + + static func truncate(_ url: URL, to size: Int) throws { + fatalError("Not implemented") + } + + static func touch(_ url: URL) throws { + fatalError("Not implemented") + } + + static func utime(_ url: URL, atime: Date, mtime: Date) throws { + fatalError("Not implemented") + } + + static func lutime(_ url: URL, atime: Date, mtime: Date) throws { + fatalError("Not implemented") + } + + static func mkfifo(_ url: URL, permissions: Int = 0o666) throws { + fatalError("Not implemented") + } + + static func identical(_ url1: URL, _ url2: URL) throws -> Bool { + fatalError("Not implemented") + } + + static func umask() -> Int { + fatalError("Not implemented") + } + + static func umask(_ mask: Int) -> Int { + fatalError("Not implemented") + } +} + +// MARK: - Pattern Matching + +public extension File { + static func fnmatch(pattern: String, path: String, flags: FnmatchFlags = []) -> Bool { + fatalError("Not implemented") + } +} + +// MARK: - Supporting Types + +public struct FileStat { + public let dev: Int // Device ID + public let ino: Int // Inode number + public let mode: Int // File mode (permissions + type) + public let nlink: Int // Number of hard links + public let uid: Int // User ID of owner + public let gid: Int // Group ID of owner + public let rdev: Int // Device ID (if special file) + public let size: Int64 // Total size in bytes + public let blksize: Int // Block size for filesystem I/O + public let blocks: Int64 // Number of 512B blocks allocated + public let atime: Date // Last access time + public let mtime: Date // Last modification time + public let ctime: Date // Last status change time + public let birthtime: Date? // Creation time (if available) +} + +public struct FnmatchFlags: OptionSet { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let pathname = FnmatchFlags(rawValue: 1 << 0) // FNM_PATHNAME + public static let noescape = FnmatchFlags(rawValue: 1 << 1) // FNM_NOESCAPE + public static let period = FnmatchFlags(rawValue: 1 << 2) // FNM_PERIOD + public static let casefold = FnmatchFlags(rawValue: 1 << 3) // FNM_CASEFOLD + public static let extglob = FnmatchFlags(rawValue: 1 << 4) // FNM_EXTGLOB + public static let dotmatch = FnmatchFlags(rawValue: 1 << 5) // FNM_DOTMATCH (custom) +} + +public enum LockOperation { + case shared // LOCK_SH + case exclusive // LOCK_EX + case unlock // LOCK_UN + case nonBlocking // LOCK_NB (can be OR'd with others) +} + +// MARK: - File Type enum + +public enum FileType: String { + case file = "file" + case directory = "directory" + case characterSpecial = "characterSpecial" + case blockSpecial = "blockSpecial" + case fifo = "fifo" + case link = "link" + case socket = "socket" + case unknown = "unknown" +} + +public extension File { + static func ftype(_ url: URL) -> FileType { + fatalError("Not implemented") + } +} \ No newline at end of file diff --git a/FileOtter/Glob.swift b/FileOtter/Glob.swift index c47ea48..b6e992e 100644 --- a/FileOtter/Glob.swift +++ b/FileOtter/Glob.swift @@ -17,7 +17,7 @@ import Foundation /// Examples: /// "src/**/*.swift" /// "/var/log/**/app*.log" -func globstar(_ pattern: String, base: URL? = nil) -> [URL] { +func globstar(_ pattern: String, base: URL? = nil) -> [String] { // Normalize and split into path components let comps = pattern.split(separator: "/", omittingEmptySubsequences: true).map(String.init) diff --git a/FileOtterTests/FileFnmatchTests.swift b/FileOtterTests/FileFnmatchTests.swift new file mode 100644 index 0000000..7a717d8 --- /dev/null +++ b/FileOtterTests/FileFnmatchTests.swift @@ -0,0 +1,129 @@ +// +// FileFnmatchTests.swift +// FileOtterTests +// +// Created by Sami Samhuri on 2025-08-19. +// + +@testable import FileOtter +import XCTest + +final class FileFnmatchTests: XCTestCase { + + // MARK: - Basic Pattern Tests + + func testExactMatch() throws { + // TODO: Implement + // File.fnmatch("cat", "cat") => true + // File.fnmatch("cat", "dog") => false + } + + func testPartialMatch() throws { + // TODO: Implement + // File.fnmatch("cat", "category") => false (must match entire string) + } + + // MARK: - Wildcard Tests + + func testStarWildcard() throws { + // TODO: Implement + // File.fnmatch("c*", "cats") => true + // File.fnmatch("c*t", "cat") => true + // File.fnmatch("c*t", "c/a/b/t") => true + } + + func testQuestionMarkWildcard() throws { + // TODO: Implement + // File.fnmatch("c?t", "cat") => true + // File.fnmatch("c??t", "cat") => false + } + + func testDoubleStarWildcard() throws { + // TODO: Implement + // File.fnmatch("**/*.rb", "main.rb") => false + // File.fnmatch("**/*.rb", "lib/song.rb") => true + // File.fnmatch("**.rb", "main.rb") => true + } + + // MARK: - Character Set Tests + + func testCharacterSet() throws { + // TODO: Implement + // File.fnmatch("ca[a-z]", "cat") => true + // File.fnmatch("ca[0-9]", "cat") => false + } + + func testNegatedCharacterSet() throws { + // TODO: Implement + // File.fnmatch("ca[^t]", "cat") => false + // File.fnmatch("ca[^t]", "cab") => true + } + + // MARK: - Escape Tests + + func testEscapedWildcard() throws { + // TODO: Implement + // File.fnmatch("\\?", "?") => true + // File.fnmatch("\\*", "*") => true + } + + func testEscapeInBrackets() throws { + // TODO: Implement + // File.fnmatch("[\\?]", "?") => true + } + + // MARK: - Flag Tests + + func testCaseFoldFlag() throws { + // TODO: Implement + // File.fnmatch("cat", "CAT", flags: []) => false + // File.fnmatch("cat", "CAT", flags: .casefold) => true + } + + func testPathnameFlag() throws { + // TODO: Implement + // File.fnmatch("*", "/", flags: []) => true + // File.fnmatch("*", "/", flags: .pathname) => false + // File.fnmatch("?", "/", flags: .pathname) => false + } + + func testPeriodFlag() throws { + // TODO: Implement + // File.fnmatch("*", ".profile", flags: []) => false (FNM_PERIOD by default) + // File.fnmatch(".*", ".profile", flags: []) => true + } + + func testDotmatchFlag() throws { + // TODO: Implement + // File.fnmatch("*", ".profile", flags: .dotmatch) => true + } + + func testNoescapeFlag() throws { + // TODO: Implement + // File.fnmatch("\\a", "a", flags: []) => true + // File.fnmatch("\\a", "\\a", flags: .noescape) => true + } + + func testExtglobFlag() throws { + // TODO: Implement + // File.fnmatch("c{at,ub}s", "cats", flags: []) => false + // File.fnmatch("c{at,ub}s", "cats", flags: .extglob) => true + // File.fnmatch("c{at,ub}s", "cubs", flags: .extglob) => true + } + + // MARK: - Complex Pattern Tests + + func testComplexGlobPatterns() throws { + // TODO: Implement + // File.fnmatch("**/foo", "a/b/c/foo", flags: .pathname) => true + // File.fnmatch("**/foo", "/a/b/c/foo", flags: .pathname) => true + // File.fnmatch("**/foo", "a/.b/c/foo", flags: .pathname) => false + // File.fnmatch("**/foo", "a/.b/c/foo", flags: [.pathname, .dotmatch]) => true + } + + func testHiddenFileMatching() throws { + // TODO: Implement + // File.fnmatch("*", "dave/.profile", flags: []) => true + // File.fnmatch("**/.*", "a/.hidden", flags: .pathname) => true + } +} \ No newline at end of file diff --git a/FileOtterTests/FileInfoTests.swift b/FileOtterTests/FileInfoTests.swift new file mode 100644 index 0000000..9d370fe --- /dev/null +++ b/FileOtterTests/FileInfoTests.swift @@ -0,0 +1,143 @@ +// +// FileInfoTests.swift +// FileOtterTests +// +// Created by Sami Samhuri on 2025-08-19. +// + +@testable import FileOtter +import XCTest + +final class FileInfoTests: XCTestCase { + var tempDir: URL! + var testFile: URL! + + override func setUpWithError() throws { + tempDir = URL.temporaryDirectory + .appendingPathComponent("FileInfoTests-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + testFile = tempDir.appendingPathComponent("test.txt") + try "Test content".write(to: testFile, atomically: true, encoding: .utf8) + } + + override func tearDownWithError() throws { + if FileManager.default.fileExists(atPath: tempDir.path) { + try FileManager.default.removeItem(at: tempDir) + } + } + + // MARK: - Time-based Tests + + func testAtime() throws { + // TODO: Implement + // File.atime(url) returns last access time + } + + func testMtime() throws { + // TODO: Implement + // File.mtime(url) returns last modification time + } + + func testCtime() throws { + // TODO: Implement + // File.ctime(url) returns last status change time + } + + func testBirthtime() throws { + // TODO: Implement + // File.birthtime(url) returns creation time + } + + func testBirthtimeThrowsOnUnsupportedPlatform() throws { + // TODO: Implement if platform doesn't support birthtime + } + + // MARK: - Size Tests + + func testSize() throws { + // TODO: Implement + // File.size(url) returns file size in bytes + } + + func testSizeThrowsForNonExistent() throws { + // TODO: Implement + } + + func testSizeForEmptyFile() throws { + // TODO: Implement + } + + // MARK: - Stat Tests + + func testStat() throws { + // TODO: Implement + // File.stat(url) returns FileStat object + } + + func testStatThrowsForNonExistent() throws { + // TODO: Implement + } + + func testLstat() throws { + // TODO: Implement + // File.lstat(url) doesn't follow symlinks + } + + func testLstatForSymlink() throws { + // TODO: Implement + // Create symlink and verify lstat returns symlink stats, not target + } + + // MARK: - Instance Method Tests + + func testInstanceAtime() throws { + // TODO: Implement + // file.atime returns access time + } + + func testInstanceMtime() throws { + // TODO: Implement + // file.mtime returns modification time + } + + func testInstanceCtime() throws { + // TODO: Implement + // file.ctime returns status change time + } + + func testInstanceBirthtime() throws { + // TODO: Implement + // file.birthtime returns creation time + } + + func testInstanceSize() throws { + // TODO: Implement + // file.size returns file size + } + + func testInstanceStat() throws { + // TODO: Implement + // file.stat() returns FileStat object + } + + func testInstanceLstat() throws { + // TODO: Implement + // file.lstat() doesn't follow symlinks + } + + // MARK: - FileStat Tests + + func testFileStatProperties() throws { + // TODO: Implement + // Verify all FileStat properties are populated correctly + } + + func testFileStatForDirectory() throws { + // TODO: Implement + } + + func testFileStatForSymlink() throws { + // TODO: Implement + } +} \ No newline at end of file diff --git a/FileOtterTests/FileOpenTests.swift b/FileOtterTests/FileOpenTests.swift new file mode 100644 index 0000000..116215f --- /dev/null +++ b/FileOtterTests/FileOpenTests.swift @@ -0,0 +1,151 @@ +// +// FileOpenTests.swift +// FileOtterTests +// +// Created by Sami Samhuri on 2025-08-19. +// + +@testable import FileOtter +import XCTest + +final class FileOpenTests: XCTestCase { + var tempDir: URL! + var testFile: URL! + + override func setUpWithError() throws { + tempDir = URL.temporaryDirectory + .appendingPathComponent("FileOpenTests-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + testFile = tempDir.appendingPathComponent("test.txt") + try "Initial content\nSecond line\n".write(to: testFile, atomically: true, encoding: .utf8) + } + + override func tearDownWithError() throws { + if FileManager.default.fileExists(atPath: tempDir.path) { + try FileManager.default.removeItem(at: tempDir) + } + } + + // MARK: - Open Mode Tests + + func testOpenReadMode() throws { + // TODO: Implement + // File(url, mode: .read) opens for reading only + } + + func testOpenWriteMode() throws { + // TODO: Implement + // File(url, mode: .write) truncates and opens for writing + } + + func testOpenAppendMode() throws { + // TODO: Implement + // File(url, mode: .append) opens for appending + } + + func testOpenReadWriteMode() throws { + // TODO: Implement + // File(url, mode: .readWrite) opens for read and write + } + + func testOpenReadWriteNewMode() throws { + // TODO: Implement + // File(url, mode: .readWriteNew) truncates and opens for read/write + } + + func testOpenReadAppendMode() throws { + // TODO: Implement + // File(url, mode: .readAppend) opens for read and append + } + + func testOpenWriteExclusiveMode() throws { + // TODO: Implement + // File(url, mode: .writeExclusive) creates new file, fails if exists + } + + // MARK: - Open Method Tests + + func testStaticOpen() throws { + // TODO: Implement + // File.open(url) returns File object + } + + func testStaticOpenWithBlock() throws { + // TODO: Implement + // File.open(url) { file in ... } auto-closes file + } + + func testStaticOpenWithBlockThrowingError() throws { + // TODO: Implement + // File.open with block closes file even on error + } + + // MARK: - File Creation Tests + + func testOpenCreatesFileInWriteMode() throws { + // TODO: Implement + // Opening non-existent file in write mode creates it + } + + func testOpenFailsInReadModeForNonExistent() throws { + // TODO: Implement + // Opening non-existent file in read mode throws error + } + + func testOpenWithPermissions() throws { + // TODO: Implement + // File(url, mode: .write, permissions: 0o644) creates with permissions + } + + // MARK: - Auto-close Tests + + func testDeinitClosesHandle() throws { + // TODO: Implement + // File handle is closed when File object is deallocated + } + + func testExplicitClose() throws { + // TODO: Implement + // file.close() explicitly closes handle + } + + func testDoubleCloseIsOK() throws { + // TODO: Implement + // Calling close() twice doesn't throw + } + + // MARK: - Locking Tests + + func testFlockShared() throws { + // TODO: Implement + // file.flock(.shared) acquires shared lock + } + + func testFlockExclusive() throws { + // TODO: Implement + // file.flock(.exclusive) acquires exclusive lock + } + + func testFlockUnlock() throws { + // TODO: Implement + // file.flock(.unlock) releases lock + } + + func testFlockNonBlocking() throws { + // TODO: Implement + // file.flock(.nonBlocking) returns immediately if can't lock + } + + // MARK: - Description Tests + + func testDescription() throws { + // TODO: Implement + // file.description returns path + } + + func testDebugDescription() throws { + // TODO: Implement + // file.debugDescription includes path and mode + } +} \ No newline at end of file diff --git a/FileOtterTests/FileOperationTests.swift b/FileOtterTests/FileOperationTests.swift new file mode 100644 index 0000000..38b1576 --- /dev/null +++ b/FileOtterTests/FileOperationTests.swift @@ -0,0 +1,187 @@ +// +// 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 { + // TODO: Implement + // File.link(source, destination) creates hard link + } + + func testLinkThrowsIfDestExists() throws { + // TODO: Implement + // File.link should not overwrite existing file + } + + func testSymlink() throws { + // TODO: Implement + // File.symlink(source, destination) creates symbolic link + } + + func testSymlinkThrowsIfDestExists() throws { + // TODO: Implement + } + + func testReadlink() throws { + // TODO: Implement + // File.readlink(url) returns target of symlink + } + + func testReadlinkThrowsForNonSymlink() throws { + // TODO: Implement + } + + // MARK: - Delete Tests + + func testUnlink() throws { + // TODO: Implement + // File.unlink(url) deletes file + } + + func testUnlinkThrowsForNonExistent() throws { + // TODO: Implement + } + + func testDelete() throws { + // TODO: Implement + // File.delete(url) is alias for unlink + } + + // MARK: - Rename Tests + + func testRename() throws { + // TODO: Implement + // File.rename(source, destination) moves/renames file + } + + func testRenameOverwritesExisting() throws { + // TODO: Implement + // rename should overwrite if dest exists + } + + func testRenameThrowsForNonExistent() throws { + // TODO: Implement + } + + // MARK: - Truncate Tests + + func testTruncate() throws { + // TODO: Implement + // File.truncate(url, size) truncates file to size + } + + func testTruncateExpands() throws { + // TODO: Implement + // truncate can expand file with zero padding + } + + func testTruncateThrowsForNonExistent() throws { + // TODO: Implement + } + + func testInstanceTruncate() throws { + // TODO: Implement + // file.truncate(size) truncates open file + } + + // MARK: - Touch Tests + + func testTouch() throws { + // TODO: Implement + // File.touch(url) updates access/modification times + } + + func testTouchCreatesFile() throws { + // TODO: Implement + // touch creates file if it doesn't exist + } + + // MARK: - utime Tests + + func testUtime() throws { + // TODO: Implement + // File.utime(url, atime, mtime) sets specific times + } + + func testUtimeFollowsSymlinks() throws { + // TODO: Implement + // utime affects target of symlink + } + + func testLutime() throws { + // TODO: Implement + // File.lutime(url, atime, mtime) sets symlink times + } + + // MARK: - mkfifo Tests + + func testMkfifo() throws { + // TODO: Implement + // File.mkfifo(url) creates named pipe + } + + func testMkfifoWithPermissions() throws { + // TODO: Implement + // File.mkfifo(url, permissions) creates with specific perms + } + + func testMkfifoThrowsIfExists() throws { + // TODO: Implement + } + + // MARK: - identical Tests + + func testIdentical() throws { + // TODO: Implement + // File.identical(url1, url2) returns true for same file + } + + func testIdenticalForHardLink() throws { + // TODO: Implement + // identical returns true for hard links to same file + } + + func testIdenticalForSymlink() throws { + // TODO: Implement + // identical returns true for symlink and target + } + + func testIdenticalForDifferent() throws { + // TODO: Implement + // identical returns false for different files + } + + func testIdenticalForSameContent() throws { + // TODO: Implement + // identical returns false for files with same content but different inodes + } +} \ No newline at end of file diff --git a/FileOtterTests/FilePathTests.swift b/FileOtterTests/FilePathTests.swift new file mode 100644 index 0000000..c1cf347 --- /dev/null +++ b/FileOtterTests/FilePathTests.swift @@ -0,0 +1,167 @@ +// +// FilePathTests.swift +// FileOtterTests +// +// Created by Sami Samhuri on 2025-08-19. +// + +@testable import FileOtter +import XCTest + +final class FilePathTests: XCTestCase { + var tempDir: URL! + + override func setUpWithError() throws { + tempDir = URL.temporaryDirectory + .appendingPathComponent("FilePathTests-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + } + + override func tearDownWithError() throws { + if FileManager.default.fileExists(atPath: tempDir.path) { + try FileManager.default.removeItem(at: tempDir) + } + } + + // MARK: - basename Tests + + func testBasename() throws { + let url1 = URL(fileURLWithPath: "/home/user/file.txt") + XCTAssertEqual(File.basename(url1), "file.txt") + + let url2 = URL(fileURLWithPath: "/home/user/dir/") + XCTAssertEqual(File.basename(url2), "dir") + + let url3 = URL(fileURLWithPath: "/") + XCTAssertEqual(File.basename(url3), "/") + + let url4 = URL(fileURLWithPath: "file.rb") + XCTAssertEqual(File.basename(url4), "file.rb") + } + + func testBasenameWithSuffix() throws { + let url = URL(fileURLWithPath: "/home/user/file.txt") + XCTAssertEqual(File.basename(url, suffix: ".txt"), "file") + XCTAssertEqual(File.basename(url, suffix: ".rb"), "file.txt") + + let url2 = URL(fileURLWithPath: "/home/user/archive.tar.gz") + XCTAssertEqual(File.basename(url2, suffix: ".gz"), "archive.tar") + XCTAssertEqual(File.basename(url2, suffix: ".tar.gz"), "archive") + } + + func testBasenameWithWildcardSuffix() throws { + let url = URL(fileURLWithPath: "/home/user/file.txt") + XCTAssertEqual(File.basename(url, suffix: ".*"), "file") + + let url2 = URL(fileURLWithPath: "/home/user/archive.tar.gz") + XCTAssertEqual(File.basename(url2, suffix: ".*"), "archive.tar") + + let url3 = URL(fileURLWithPath: "/home/user/noext") + XCTAssertEqual(File.basename(url3, suffix: ".*"), "noext") + } + + // MARK: - dirname Tests + + func testDirname() throws { + let url1 = URL(fileURLWithPath: "/home/user/file.txt") + XCTAssertEqual(File.dirname(url1).path, "/home/user") + + let url2 = URL(fileURLWithPath: "/home/user/dir/") + XCTAssertEqual(File.dirname(url2).path, "/home/user") + + let url3 = URL(fileURLWithPath: "/file.txt") + XCTAssertEqual(File.dirname(url3).path, "/") + + let url4 = URL(fileURLWithPath: "file.txt") + XCTAssertEqual(File.dirname(url4).path, ".") + } + + func testDirnameWithLevel() throws { + let url = URL(fileURLWithPath: "/home/user/dir/file.txt") + XCTAssertEqual(File.dirname(url, level: 1).path, "/home/user/dir") + XCTAssertEqual(File.dirname(url, level: 2).path, "/home/user") + XCTAssertEqual(File.dirname(url, level: 3).path, "/home") + XCTAssertEqual(File.dirname(url, level: 4).path, "/") + XCTAssertEqual(File.dirname(url, level: 5).path, "/") // Can't go beyond root + } + + // MARK: - extname Tests + + func testExtname() throws { + XCTAssertEqual(File.extname(URL(fileURLWithPath: "test.rb")), ".rb") + XCTAssertEqual(File.extname(URL(fileURLWithPath: "a/b/d/test.rb")), ".rb") + XCTAssertEqual(File.extname(URL(fileURLWithPath: ".a/b/d/test.rb")), ".rb") + XCTAssertEqual(File.extname(URL(fileURLWithPath: "test")), "") + XCTAssertEqual(File.extname(URL(fileURLWithPath: "test.tar.gz")), ".gz") + } + + func testExtnameWithDotfile() throws { + XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile")), "") + XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile.sh")), ".sh") + XCTAssertEqual(File.extname(URL(fileURLWithPath: "/home/user/.bashrc")), "") + XCTAssertEqual(File.extname(URL(fileURLWithPath: "/home/user/.config.bak")), ".bak") + } + + // MARK: - split Tests + + func testSplit() throws { + // TODO: Implement + // File.split("/home/user/file.txt") => ("/home/user", "file.txt") + } + + // MARK: - join Tests + + func testJoin() throws { + // TODO: Implement + // File.join("usr", "mail", "gumby") => "usr/mail/gumby" + } + + func testJoinWithArray() throws { + // TODO: Implement + } + + // MARK: - absolutePath Tests + + func testAbsolutePath() throws { + // TODO: Implement + // Converts relative to absolute + } + + func testAbsolutePathWithBase() throws { + // TODO: Implement + } + + // MARK: - expandPath Tests + + func testExpandPath() throws { + // TODO: Implement + // File.expandPath("~") => home directory + // File.expandPath("~/Documents") => home/Documents + } + + func testExpandPathWithRelative() throws { + // TODO: Implement + } + + // MARK: - realpath Tests + + func testRealpath() throws { + // TODO: Implement + // Resolves symlinks, all components must exist + } + + func testRealpathThrowsForNonExistent() throws { + // TODO: Implement + } + + // MARK: - realdirpath Tests + + func testRealdirpath() throws { + // TODO: Implement + // Resolves symlinks, last component may not exist + } + + func testRealdirpathWithNonExistentLast() throws { + // TODO: Implement + } +} \ No newline at end of file diff --git a/FileOtterTests/FilePermissionTests.swift b/FileOtterTests/FilePermissionTests.swift new file mode 100644 index 0000000..231ae2a --- /dev/null +++ b/FileOtterTests/FilePermissionTests.swift @@ -0,0 +1,166 @@ +// +// FilePermissionTests.swift +// FileOtterTests +// +// Created by Sami Samhuri on 2025-08-19. +// + +@testable import FileOtter +import XCTest + +final class FilePermissionTests: XCTestCase { + var tempDir: URL! + var testFile: URL! + var readOnlyFile: URL! + var executableFile: URL! + + override func setUpWithError() throws { + tempDir = URL.temporaryDirectory + .appendingPathComponent("FilePermissionTests-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + testFile = tempDir.appendingPathComponent("test.txt") + try "Test content".write(to: testFile, atomically: true, encoding: .utf8) + + readOnlyFile = tempDir.appendingPathComponent("readonly.txt") + try "Read only".write(to: readOnlyFile, atomically: true, encoding: .utf8) + + executableFile = tempDir.appendingPathComponent("script.sh") + try "#!/bin/sh\necho hello".write(to: executableFile, atomically: true, encoding: .utf8) + } + + override func tearDownWithError() throws { + if FileManager.default.fileExists(atPath: tempDir.path) { + try FileManager.default.removeItem(at: tempDir) + } + } + + // MARK: - Basic Permission Tests + + func testIsReadable() throws { + // TODO: Implement + // File.isReadable(url) returns true for readable files + } + + func testIsWritable() throws { + // TODO: Implement + // File.isWritable(url) returns true for writable files + } + + func testIsExecutable() throws { + // TODO: Implement + // File.isExecutable(url) returns true for executable files + } + + func testIsExecutableForNonExecutable() throws { + // TODO: Implement + // File.isExecutable(url) returns false for non-executable + } + + // MARK: - Ownership Tests + + func testIsOwned() throws { + // TODO: Implement + // File.isOwned(url) returns true for files owned by effective user + } + + func testIsGroupOwned() throws { + // TODO: Implement + // File.isGroupOwned(url) returns true for files owned by effective group + } + + // MARK: - World Permission Tests + + func testIsWorldReadable() throws { + // TODO: Implement + // File.isWorldReadable(url) returns permission bits if world readable + } + + func testIsWorldReadableForPrivate() throws { + // TODO: Implement + // File.isWorldReadable(url) returns nil if not world readable + } + + func testIsWorldWritable() throws { + // TODO: Implement + // File.isWorldWritable(url) returns permission bits if world writable + } + + func testIsWorldWritableForProtected() throws { + // TODO: Implement + // File.isWorldWritable(url) returns nil if not world writable + } + + // MARK: - Special Bit Tests + + func testIsSetuid() throws { + // TODO: Implement + // File.isSetuid(url) returns true if setuid bit is set + } + + func testIsSetgid() throws { + // TODO: Implement + // File.isSetgid(url) returns true if setgid bit is set + } + + func testIsSticky() throws { + // TODO: Implement + // File.isSticky(url) returns true if sticky bit is set + } + + // MARK: - chmod Tests + + func testChmod() throws { + // TODO: Implement + // File.chmod(url, permissions) changes file permissions + } + + func testChmodThrowsForNonExistent() throws { + // TODO: Implement + } + + func testLchmod() throws { + // TODO: Implement + // File.lchmod(url, permissions) changes symlink permissions + } + + func testInstanceChmod() throws { + // TODO: Implement + // file.chmod(permissions) changes open file permissions + } + + // MARK: - chown Tests + + func testChown() throws { + // TODO: Implement + // File.chown(url, owner, group) changes ownership + // Note: May require special privileges + } + + func testChownWithNilValues() throws { + // TODO: Implement + // nil owner or group means don't change that value + } + + func testLchown() throws { + // TODO: Implement + // File.lchown(url, owner, group) changes symlink ownership + } + + func testInstanceChown() throws { + // TODO: Implement + // file.chown(owner, group) changes open file ownership + } + + // MARK: - umask Tests + + func testUmask() throws { + // TODO: Implement + // File.umask() returns current umask + } + + func testUmaskSet() throws { + // TODO: Implement + // File.umask(mask) sets umask and returns previous value + } +} \ No newline at end of file diff --git a/FileOtterTests/FileTypeTests.swift b/FileOtterTests/FileTypeTests.swift new file mode 100644 index 0000000..1f40fcf --- /dev/null +++ b/FileOtterTests/FileTypeTests.swift @@ -0,0 +1,195 @@ +// +// FileTypeTests.swift +// FileOtterTests +// +// Created by Sami Samhuri on 2025-08-19. +// + +@testable import FileOtter +import XCTest + +final class FileTypeTests: XCTestCase { + var tempDir: URL! + var testFile: URL! + var testDir: URL! + + override func setUpWithError() throws { + tempDir = URL.temporaryDirectory + .appendingPathComponent("FileTypeTests-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + testFile = tempDir.appendingPathComponent("test.txt") + try "Test content".write(to: testFile, atomically: true, encoding: .utf8) + + testDir = tempDir.appendingPathComponent("subdir") + try FileManager.default.createDirectory(at: testDir, withIntermediateDirectories: true) + } + + override func tearDownWithError() throws { + if FileManager.default.fileExists(atPath: tempDir.path) { + try FileManager.default.removeItem(at: tempDir) + } + } + + // MARK: - Existence Tests + + func testExists() throws { + // Test with existing file + XCTAssertTrue(File.exists(testFile)) + + // Test with existing directory + XCTAssertTrue(File.exists(testDir)) + XCTAssertTrue(File.exists(tempDir)) + } + + func testExistsForNonExistent() throws { + let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt") + XCTAssertFalse(File.exists(nonExistent)) + + let nonExistentDir = tempDir.appendingPathComponent("no-such-dir") + XCTAssertFalse(File.exists(nonExistentDir)) + } + + func testExistsForDirectory() throws { + // File.exists returns true for directories (like Ruby) + XCTAssertTrue(File.exists(tempDir)) + XCTAssertTrue(File.exists(testDir)) + + // Also test system directories + XCTAssertTrue(File.exists(URL(fileURLWithPath: "/tmp"))) + XCTAssertTrue(File.exists(URL(fileURLWithPath: "/"))) + } + + // MARK: - File Type Tests + + func testIsFile() throws { + // Returns true for regular files + XCTAssertTrue(File.isFile(testFile)) + + // Create another test file + let anotherFile = tempDir.appendingPathComponent("another.txt") + try "content".write(to: anotherFile, atomically: true, encoding: .utf8) + XCTAssertTrue(File.isFile(anotherFile)) + } + + func testIsFileForDirectory() throws { + // Returns false for directories + XCTAssertFalse(File.isFile(testDir)) + XCTAssertFalse(File.isFile(tempDir)) + XCTAssertFalse(File.isFile(URL(fileURLWithPath: "/"))) + + // Returns false for non-existent paths + let nonExistent = tempDir.appendingPathComponent("no-such-file.txt") + XCTAssertFalse(File.isFile(nonExistent)) + } + + func testIsDirectory() throws { + // Returns true for directories + XCTAssertTrue(File.isDirectory(testDir)) + XCTAssertTrue(File.isDirectory(tempDir)) + XCTAssertTrue(File.isDirectory(URL(fileURLWithPath: "/"))) + XCTAssertTrue(File.isDirectory(URL(fileURLWithPath: "/tmp"))) + } + + func testIsDirectoryForFile() throws { + // Returns false for files + XCTAssertFalse(File.isDirectory(testFile)) + + // Returns false for non-existent paths + let nonExistent = tempDir.appendingPathComponent("no-such-dir") + XCTAssertFalse(File.isDirectory(nonExistent)) + } + + func testIsSymlink() throws { + // TODO: Implement + // Create symlink and test File.isSymlink(url) + } + + func testIsSymlinkForRegularFile() throws { + // TODO: Implement + // File.isSymlink(url) returns false for regular files + } + + func testIsBlockDevice() throws { + // TODO: Implement + // File.isBlockDevice(url) - may need special test file + } + + func testIsCharDevice() throws { + // TODO: Implement + // File.isCharDevice(url) - test with /dev/null or similar + } + + func testIsPipe() throws { + // TODO: Implement + // File.isPipe(url) - create FIFO and test + } + + func testIsSocket() throws { + // TODO: Implement + // File.isSocket(url) - create socket and test + } + + // MARK: - Empty/Zero Tests + + func testIsEmpty() throws { + // TODO: Implement + // File.isEmpty(url) returns true for empty file + } + + func testIsEmptyForNonEmpty() throws { + // TODO: Implement + // File.isEmpty(url) returns false for non-empty file + } + + func testIsEmptyThrowsForNonExistent() throws { + // TODO: Implement + } + + func testIsZero() throws { + // TODO: Implement + // File.isZero(url) is alias for isEmpty + } + + // MARK: - ftype Tests + + func testFtypeForFile() throws { + // TODO: Implement + // File.ftype(url) returns .file + } + + func testFtypeForDirectory() throws { + // TODO: Implement + // File.ftype(url) returns .directory + } + + func testFtypeForSymlink() throws { + // TODO: Implement + // File.ftype(url) returns .link + } + + func testFtypeForCharDevice() throws { + // TODO: Implement + // File.ftype(url) returns .characterSpecial + } + + func testFtypeForBlockDevice() throws { + // TODO: Implement + // File.ftype(url) returns .blockSpecial + } + + func testFtypeForFifo() throws { + // TODO: Implement + // File.ftype(url) returns .fifo + } + + func testFtypeForSocket() throws { + // TODO: Implement + // File.ftype(url) returns .socket + } + + func testFtypeForUnknown() throws { + // TODO: Implement + // File.ftype(url) returns .unknown for unrecognized types + } +} \ No newline at end of file