From 993cc73ec07ed305fdd93bd34fb4306702ed6e2f Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Thu, 28 Aug 2025 08:52:26 -0700 Subject: [PATCH] WIP: Implement more of File --- FileOtter/Dir.swift | 6 +- FileOtter/File.swift | 515 +++++++++++++++-------- FileOtter/Glob.swift | 6 +- FileOtterTests/DirTests.swift | 10 +- FileOtterTests/FileFnmatchTests.swift | 47 +-- FileOtterTests/FileInfoTests.swift | 118 ++++-- FileOtterTests/FileOpenTests.swift | 64 +-- FileOtterTests/FileOperationTests.swift | 128 +++--- FileOtterTests/FilePathTests.swift | 180 +++++--- FileOtterTests/FilePermissionTests.swift | 201 ++++++--- FileOtterTests/FileTypeTests.swift | 193 ++++++--- 11 files changed, 925 insertions(+), 543 deletions(-) diff --git a/FileOtter/Dir.swift b/FileOtter/Dir.swift index c9c24f9..0b539b9 100644 --- a/FileOtter/Dir.swift +++ b/FileOtter/Dir.swift @@ -98,7 +98,7 @@ public extension Dir { try FileManager.default.createDirectory( at: url, withIntermediateDirectories: false, - attributes: attributes + attributes: attributes, ) } @@ -110,7 +110,7 @@ public extension Dir { try FileManager.default.createDirectory( at: tmpDir, withIntermediateDirectories: true, - attributes: [.posixPermissions: 0o700] + attributes: [.posixPermissions: 0o700], ) return tmpDir } @@ -119,7 +119,7 @@ public extension Dir { static func mktmpdir( prefix: String = "d", suffix: String = "", - _ block: (URL) throws -> T + _ block: (URL) throws -> T, ) throws -> T { let tmpDir = try mktmpdir(prefix: prefix, suffix: suffix) defer { diff --git a/FileOtter/File.swift b/FileOtter/File.swift index 66a9f1d..dcedc1d 100644 --- a/FileOtter/File.swift +++ b/FileOtter/File.swift @@ -5,6 +5,7 @@ // Created by Sami Samhuri on 2025-08-19. // +import Darwin import Foundation // MARK: - File Class @@ -15,101 +16,101 @@ 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 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 { + + public init(url: URL, mode: Mode = .read, permissions _: Int = 0o666) throws { self.url = url self.mode = mode - self.handle = FileHandle() // TODO: Implement proper opening + 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 { + + 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 { + public static func open(url _: URL, mode _: Mode = .read, permissions _: Int = 0o666, _: (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 { + + public func chmod(_: Int) throws { fatalError("Not implemented") } - - public func chown(owner: Int? = nil, group: Int? = nil) throws { + + public func chown(owner _: Int? = nil, group _: Int? = nil) throws { fatalError("Not implemented") } - - public func truncate(to size: Int) throws { + + public func truncate(to _: Int) throws { fatalError("Not implemented") } - - public func flock(_ operation: LockOperation) throws { + + public func flock(_: LockOperation) throws { fatalError("Not implemented") } - - public func stat() throws -> FileStat { + + public func fileStat() throws -> FileStat { fatalError("Not implemented") } - - public func lstat() throws -> FileStat { + + public func fileLstat() throws -> FileStat { fatalError("Not implemented") } - + public func close() throws { fatalError("Not implemented") } - + // MARK: - CustomStringConvertible - + public var description: String { url.path } - + public var debugDescription: String { "" } @@ -123,70 +124,70 @@ public extension File { 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 { + guard let 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) { let dir = url.deletingLastPathComponent() let name = url.lastPathComponent - + // Handle root path special case if url.path == "/" { return (url, "") } - + return (dir, name) } - + static func join(_ components: String...) -> URL { join(components) } - + static func join(_ components: [String]) -> URL { // Filter out empty components let nonEmptyComponents = components.filter { !$0.isEmpty } - + guard !nonEmptyComponents.isEmpty else { return URL(fileURLWithPath: ".") } - + // Start with the first component to preserve absolute/relative nature var result = URL(fileURLWithPath: nonEmptyComponents[0]) - + // Append remaining components for component in nonEmptyComponents.dropFirst() { // Remove leading/trailing slashes from component before appending @@ -195,24 +196,63 @@ public extension File { result.appendPathComponent(trimmed) } } - + return result } - - static func absolutePath(_ url: URL, relativeTo base: URL? = nil) -> URL { - fatalError("Not implemented") + + static func absolutePath(_ url: URL) -> URL { + // URL(fileURLWithPath:) already makes relative paths absolute using current directory + // We just need to normalize the path (resolves . and .. but NOT symlinks) + url.standardized } - + static func expandPath(_ path: String) -> URL { - fatalError("Not implemented") + // Expand tilde to home directory + let expanded = (path as NSString).expandingTildeInPath + return URL(fileURLWithPath: expanded) } - + static func realpath(_ url: URL) throws -> URL { - fatalError("Not implemented") + // Resolve all symbolic links in the path + // All components must exist for this to work + let path = url.path + + // Check if file exists + guard FileManager.default.fileExists(atPath: path) else { + throw CocoaError(.fileNoSuchFile, userInfo: [NSFilePathErrorKey: path]) + } + + // Use standardizedFileURL to resolve symlinks and normalize the path + // This resolves .., ., and symlinks + return url.resolvingSymlinksInPath() } - + static func realdirpath(_ url: URL) throws -> URL { - fatalError("Not implemented") + // Similar to realpath but the last component may not exist + let parentURL = url.deletingLastPathComponent() + let lastComponent = url.lastPathComponent + + // If we're at root or parent doesn't exist, just standardize + if parentURL.path == "/" || parentURL.path.isEmpty { + return url.standardizedFileURL + } + + // Check if the full path exists (including the last component) + if FileManager.default.fileExists(atPath: url.path) { + // If the full path exists, resolve all symlinks + return url.resolvingSymlinksInPath() + } + + // Only the parent needs to exist, last component may not + let resolvedParent: URL = if FileManager.default.fileExists(atPath: parentURL.path) { + parentURL.resolvingSymlinksInPath() + } else { + // Parent doesn't exist, try to resolve what we can recursively + try realdirpath(parentURL) + } + + // Append the last component (which may not exist) + return resolvedParent.appendingPathComponent(lastComponent) } } @@ -223,17 +263,17 @@ public extension File { // Note: On macOS, access time updates may be disabled for performance // You can check with: mount | grep noatime let attributes = try FileManager.default.attributesOfItem(atPath: url.path) - + // Try to get access date if available if let accessDate = attributes[.modificationDate] as? Date { // FileManager doesn't expose access time directly, using modification as fallback // For true access time, would need to use stat() system call return accessDate } - + throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path]) } - + static func mtime(_ url: URL) throws -> Date { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) guard let modDate = attributes[.modificationDate] as? Date else { @@ -241,20 +281,20 @@ public extension File { } return modDate } - + static func ctime(_ url: URL) throws -> Date { // Status change time - on macOS this is often the same as mtime // For true ctime, would need to use stat() system call let attributes = try FileManager.default.attributesOfItem(atPath: url.path) - + // Try to use creation date as a proxy for ctime on macOS if let changeDate = attributes[.modificationDate] as? Date { return changeDate } - + throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path]) } - + static func birthtime(_ url: URL) throws -> Date { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) guard let creationDate = attributes[.creationDate] as? Date else { @@ -262,7 +302,7 @@ public extension File { } return creationDate } - + static func size(_ url: URL) throws -> Int { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) guard let fileSize = attributes[.size] as? NSNumber else { @@ -270,13 +310,57 @@ public extension File { } return fileSize.intValue } - - static func stat(_ url: URL) throws -> FileStat { - fatalError("Not implemented") + + static func fileStatus(_ url: URL) throws -> FileStat { + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + + guard result == 0 else { + throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path]) + } + + return FileStat( + dev: Int(statBuf.st_dev), + ino: Int(statBuf.st_ino), + mode: Int(statBuf.st_mode), + nlink: Int(statBuf.st_nlink), + uid: Int(statBuf.st_uid), + gid: Int(statBuf.st_gid), + rdev: Int(statBuf.st_rdev), + size: Int64(statBuf.st_size), + blksize: Int(statBuf.st_blksize), + blocks: Int64(statBuf.st_blocks), + atime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_atimespec.tv_sec)), + mtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_mtimespec.tv_sec)), + ctime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_ctimespec.tv_sec)), + birthtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_birthtimespec.tv_sec)), + ) } - - static func lstat(_ url: URL) throws -> FileStat { - fatalError("Not implemented") + + static func linkStatus(_ url: URL) throws -> FileStat { + var statBuf = stat() + let result = url.path.withCString { lstat($0, &statBuf) } + + guard result == 0 else { + throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path]) + } + + return FileStat( + dev: Int(statBuf.st_dev), + ino: Int(statBuf.st_ino), + mode: Int(statBuf.st_mode), + nlink: Int(statBuf.st_nlink), + uid: Int(statBuf.st_uid), + gid: Int(statBuf.st_gid), + rdev: Int(statBuf.st_rdev), + size: Int64(statBuf.st_size), + blksize: Int(statBuf.st_blksize), + blocks: Int64(statBuf.st_blocks), + atime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_atimespec.tv_sec)), + mtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_mtimespec.tv_sec)), + ctime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_ctimespec.tv_sec)), + birthtime: Date(timeIntervalSince1970: TimeInterval(statBuf.st_birthtimespec.tv_sec)), + ) } } @@ -286,44 +370,59 @@ 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") + var statBuf = stat() + let result = url.path.withCString { lstat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_IFMT) == S_IFLNK } - + static func isBlockDevice(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_IFMT) == S_IFBLK } - + static func isCharDevice(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_IFMT) == S_IFCHR } - + static func isPipe(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_IFMT) == S_IFIFO } - + static func isSocket(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_IFMT) == S_IFSOCK } - + static func isEmpty(_ url: URL) throws -> Bool { - let size = try self.size(url) + let size = try size(url) return size == 0 } - + // Ruby aliases static func isZero(_ url: URL) throws -> Bool { try isEmpty(url) @@ -334,101 +433,132 @@ public extension File { public extension File { static func isReadable(_ url: URL) -> Bool { - fatalError("Not implemented") + url.path.withCString { access($0, R_OK) } == 0 } - + static func isWritable(_ url: URL) -> Bool { - fatalError("Not implemented") + url.path.withCString { access($0, W_OK) } == 0 } - + static func isExecutable(_ url: URL) -> Bool { - fatalError("Not implemented") + url.path.withCString { access($0, X_OK) } == 0 } - + static func isOwned(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return statBuf.st_uid == getuid() } - + static func isGroupOwned(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return statBuf.st_gid == getgid() } - + static func isWorldReadable(_ url: URL) -> Int? { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return nil } + + // Check if world readable (other read bit) + if (statBuf.st_mode & S_IROTH) != 0 { + return Int(statBuf.st_mode & 0o777) + } + return nil } - + static func isWorldWritable(_ url: URL) -> Int? { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return nil } + + // Check if world writable (other write bit) + if (statBuf.st_mode & S_IWOTH) != 0 { + return Int(statBuf.st_mode & 0o777) + } + return nil } - + static func isSetuid(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_ISUID) != 0 } - + static func isSetgid(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_ISGID) != 0 } - + static func isSticky(_ url: URL) -> Bool { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { stat($0, &statBuf) } + guard result == 0 else { return false } + return (statBuf.st_mode & S_ISVTX) != 0 } } // MARK: - Static File Operations public extension File { - static func chmod(_ url: URL, permissions: Int) throws { + static func chmod(_: URL, permissions _: Int) throws { fatalError("Not implemented") } - - static func chown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws { + + static func chown(_: URL, owner _: Int? = nil, group _: Int? = nil) throws { fatalError("Not implemented") } - - static func lchmod(_ url: URL, permissions: Int) throws { + + static func lchmod(_: URL, permissions _: Int) throws { fatalError("Not implemented") } - - static func lchown(_ url: URL, owner: Int? = nil, group: Int? = nil) throws { + + static func lchown(_: URL, owner _: Int? = nil, group _: Int? = nil) throws { fatalError("Not implemented") } - - static func link(source: URL, destination: URL) throws { + + static func link(source _: URL, destination _: URL) throws { fatalError("Not implemented") } - + static func symlink(source: URL, destination: URL) throws { try FileManager.default.createSymbolicLink(at: destination, withDestinationURL: source) } - + static func readlink(_ url: URL) throws -> URL { let path = try FileManager.default.destinationOfSymbolicLink(atPath: url.path) return URL(fileURLWithPath: path) } - + static func unlink(_ url: URL) throws { try FileManager.default.removeItem(at: url) } - + static func delete(_ url: URL) throws { try unlink(url) } - + static func rename(source: URL, destination: URL) throws { // 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 + at: destination, withItemAt: source, backupItemName: nil, resultingItemURL: nil, ) } - - static func truncate(_ url: URL, to size: Int) throws { + + static func truncate(_: URL, to _: Int) throws { fatalError("Not implemented") } - + static func touch(_ url: URL) throws { let fm = FileManager.default - + if fm.fileExists(atPath: url.path) { // Update modification time to current time try fm.setAttributes([.modificationDate: Date()], ofItemAtPath: url.path) @@ -437,28 +567,28 @@ public extension File { fm.createFile(atPath: url.path, contents: nil, attributes: nil) } } - - static func utime(_ url: URL, atime: Date, mtime: Date) throws { + + static func utime(_: URL, atime _: Date, mtime _: Date) throws { fatalError("Not implemented") } - - static func lutime(_ url: URL, atime: Date, mtime: Date) throws { + + static func lutime(_: URL, atime _: Date, mtime _: Date) throws { fatalError("Not implemented") } - - static func mkfifo(_ url: URL, permissions: Int = 0o666) throws { + + static func mkfifo(_: URL, permissions _: Int = 0o666) throws { fatalError("Not implemented") } - - static func identical(_ url1: URL, _ url2: URL) throws -> Bool { + + static func identical(_: URL, _: URL) throws -> Bool { fatalError("Not implemented") } - + static func umask() -> Int { fatalError("Not implemented") } - - static func umask(_ mask: Int) -> Int { + + static func umask(_: Int) -> Int { fatalError("Not implemented") } } @@ -466,7 +596,7 @@ public extension File { // MARK: - Pattern Matching public extension File { - static func fnmatch(pattern: String, path: String, flags: FnmatchFlags = []) -> Bool { + static func fnmatch(pattern _: String, path _: String, flags _: FnmatchFlags = []) -> Bool { fatalError("Not implemented") } } @@ -474,59 +604,80 @@ public extension File { // 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 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 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) + 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" + case file + case directory + case characterSpecial + case blockSpecial + case fifo + case link + case socket + case unknown } public extension File { static func ftype(_ url: URL) -> FileType { - fatalError("Not implemented") + var statBuf = stat() + let result = url.path.withCString { lstat($0, &statBuf) } + guard result == 0 else { return .unknown } + + switch statBuf.st_mode & S_IFMT { + case S_IFREG: + return .file + case S_IFDIR: + return .directory + case S_IFLNK: + return .link + case S_IFBLK: + return .blockSpecial + case S_IFCHR: + return .characterSpecial + case S_IFIFO: + return .fifo + case S_IFSOCK: + return .socket + default: + return .unknown + } } } diff --git a/FileOtter/Glob.swift b/FileOtter/Glob.swift index b6e992e..d38680b 100644 --- a/FileOtter/Glob.swift +++ b/FileOtter/Glob.swift @@ -23,7 +23,7 @@ func globstar(_ pattern: String, base: URL? = nil) -> [String] { var results: [String] = [] var seenDirs = Set() // canonical paths to avoid cycles if symlinks appear - + // Cache frequently used objects let fm = FileManager.default let globMetaChars = CharacterSet(charactersIn: "*?[") @@ -79,7 +79,7 @@ func globstar(_ pattern: String, base: URL? = nil) -> [String] { seenDirs.insert(key) if isDir(dirPath) { - let dirPathNS = dirPath as NSString // Cache the NSString conversion + let dirPathNS = dirPath as NSString // Cache the NSString conversion for entry in listDir(dirPath) { let child = dirPathNS.appendingPathComponent(entry) if isDir(child) { @@ -102,7 +102,7 @@ func globstar(_ pattern: String, base: URL? = nil) -> [String] { // Segment glob (*, ?, []) matches names in this directory level only let dirPath = base.isEmpty ? "/" : base if !isDir(dirPath) { return } - let dirPathNS = dirPath as NSString // Cache the NSString conversion + let dirPathNS = dirPath as NSString // Cache the NSString conversion for entry in listDir(dirPath) { if matchSegment(entry, pat: part) { let next = dirPathNS.appendingPathComponent(entry) diff --git a/FileOtterTests/DirTests.swift b/FileOtterTests/DirTests.swift index bc363c4..fae755f 100644 --- a/FileOtterTests/DirTests.swift +++ b/FileOtterTests/DirTests.swift @@ -1,5 +1,5 @@ // -// FileOtterTests.swift +// DirTests.swift // FileOtterTests // // Created by Sami Samhuri on 2024-04-24. @@ -145,7 +145,7 @@ final class DirTests: XCTestCase { let children = try Dir.children(tempDir) XCTAssertEqual(children.count, 3) - let childNames = children.map { $0.lastPathComponent }.sorted() + let childNames = children.map(\.lastPathComponent).sorted() XCTAssertEqual(childNames, ["file1.txt", "file2.txt", "subdir"]) } @@ -240,7 +240,7 @@ final class DirTests: XCTestCase { let results = Dir.glob(base: tempDir, "*.txt") XCTAssertEqual(results.count, 2) - let filenames = results.map { $0.lastPathComponent }.sorted() + let filenames = results.map(\.lastPathComponent).sorted() XCTAssertEqual(filenames, ["file1.txt", "file2.txt"]) } @@ -266,7 +266,7 @@ final class DirTests: XCTestCase { let results = Dir.glob(base: tempDir, "??.txt") XCTAssertEqual(results.count, 2) - let filenames = results.map { $0.lastPathComponent }.sorted() + let filenames = results.map(\.lastPathComponent).sorted() XCTAssertEqual(filenames, ["a1.txt", "b2.txt"]) } @@ -464,7 +464,7 @@ final class DirTests: XCTestCase { XCTAssertEqual(result, "block result") XCTAssertTrue(fileCreated) - if let tmpDirInBlock = tmpDirInBlock { + if let tmpDirInBlock { XCTAssertFalse(FileManager.default.fileExists(atPath: tmpDirInBlock.path)) } } diff --git a/FileOtterTests/FileFnmatchTests.swift b/FileOtterTests/FileFnmatchTests.swift index 7a717d8..11c9ddb 100644 --- a/FileOtterTests/FileFnmatchTests.swift +++ b/FileOtterTests/FileFnmatchTests.swift @@ -9,110 +9,109 @@ 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 @@ -120,10 +119,10 @@ final class FileFnmatchTests: XCTestCase { // 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 index cfc499d..8bde86f 100644 --- a/FileOtterTests/FileInfoTests.swift +++ b/FileOtterTests/FileInfoTests.swift @@ -11,51 +11,51 @@ 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 { let atime = try File.atime(testFile) XCTAssertNotNil(atime) // Access time should be recent (within last hour) XCTAssertLessThan(Date().timeIntervalSince(atime), 3600) } - + func testMtime() throws { // Get initial mtime let initialMtime = try File.mtime(testFile) - + // Wait a moment and modify the file Thread.sleep(forTimeInterval: 0.1) try "Modified content".write(to: testFile, atomically: true, encoding: .utf8) - + // mtime should be updated let newMtime = try File.mtime(testFile) XCTAssertGreaterThan(newMtime, initialMtime) } - + func testCtime() throws { let ctime = try File.ctime(testFile) XCTAssertNotNil(ctime) // Status change time should be recent XCTAssertLessThan(Date().timeIntervalSince(ctime), 3600) } - + func testBirthtime() throws { // Create a new file let newFile = tempDir.appendingPathComponent("birthtime-test.txt") @@ -64,24 +64,24 @@ final class FileInfoTests: XCTestCase { try "content".write(to: newFile, atomically: true, encoding: .utf8) Thread.sleep(forTimeInterval: 0.01) let afterCreation = Date() - + let birthtime = try File.birthtime(newFile) XCTAssertGreaterThanOrEqual(birthtime, beforeCreation) XCTAssertLessThanOrEqual(birthtime, afterCreation) } - + func testBirthtimeThrowsOnUnsupportedPlatform() throws { // TODO: Implement if platform doesn't support birthtime } - + // MARK: - Size Tests - + func testSize() throws { // Test with known content let content = "Test content" let expectedSize = content.data(using: .utf8)!.count XCTAssertEqual(try File.size(testFile), expectedSize) - + // Test with larger file let largeFile = tempDir.appendingPathComponent("large.txt") let largeContent = String(repeating: "Hello World! ", count: 100) @@ -89,88 +89,122 @@ final class FileInfoTests: XCTestCase { let largeExpectedSize = largeContent.data(using: .utf8)!.count XCTAssertEqual(try File.size(largeFile), largeExpectedSize) } - + func testSizeThrowsForNonExistent() throws { let nonExistent = tempDir.appendingPathComponent("no-such-file.txt") XCTAssertThrowsError(try File.size(nonExistent)) } - + func testSizeForEmptyFile() throws { let emptyFile = tempDir.appendingPathComponent("empty.txt") try "".write(to: emptyFile, atomically: true, encoding: .utf8) XCTAssertEqual(try File.size(emptyFile), 0) } - + // MARK: - Stat Tests - + func testStat() throws { - // TODO: Implement - // File.stat(url) returns FileStat object + let stat = try File.fileStatus(testFile) + + // Verify basic properties + XCTAssertGreaterThan(stat.ino, 0) // inode should be positive + XCTAssertGreaterThan(stat.uid, 0) // uid should be positive + XCTAssertGreaterThan(stat.gid, 0) // gid should be positive + XCTAssertEqual(stat.size, 12) // "Test content" is 12 bytes + + // Verify times are reasonable + XCTAssertLessThan(Date().timeIntervalSince(stat.mtime), 3600) + XCTAssertLessThan(Date().timeIntervalSince(stat.atime), 3600) + XCTAssertNotNil(stat.birthtime) } - + func testStatThrowsForNonExistent() throws { - // TODO: Implement + let nonExistent = tempDir.appendingPathComponent("nonexistent") + XCTAssertThrowsError(try File.fileStatus(nonExistent)) } - + func testLstat() throws { - // TODO: Implement - // File.lstat(url) doesn't follow symlinks + // For regular files, lstat should be same as stat + let lstat = try File.linkStatus(testFile) + let stat = try File.fileStatus(testFile) + + XCTAssertEqual(lstat.size, stat.size) + XCTAssertEqual(lstat.ino, stat.ino) + XCTAssertEqual(lstat.mode, stat.mode) } - + func testLstatForSymlink() throws { - // TODO: Implement - // Create symlink and verify lstat returns symlink stats, not target + // Create a larger target file + let targetFile = tempDir.appendingPathComponent("target.txt") + let targetContent = "This is the target file content" + try targetContent.write(to: targetFile, atomically: true, encoding: .utf8) + + // Create symlink + let symlinkURL = tempDir.appendingPathComponent("symlink.txt") + try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: targetFile) + + let lstat = try File.linkStatus(symlinkURL) + let stat = try File.fileStatus(symlinkURL) + + // lstat should return symlink's own stats (smaller size) + // stat should follow the symlink to the target (larger size) + XCTAssertNotEqual(lstat.size, stat.size) + XCTAssertEqual(stat.size, Int64(targetContent.data(using: .utf8)!.count)) + + // lstat should indicate it's a symlink via mode + let isLink = (lstat.mode & 0o170000) == 0o120000 // S_IFLNK + XCTAssertTrue(isLink) } - + // 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 index 116215f..7b3429d 100644 --- a/FileOtterTests/FileOpenTests.swift +++ b/FileOtterTests/FileOpenTests.swift @@ -11,141 +11,141 @@ 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 index 64d7fef..6821a79 100644 --- a/FileOtterTests/FileOperationTests.swift +++ b/FileOtterTests/FileOperationTests.swift @@ -12,270 +12,270 @@ 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 { // 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 { // 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 { // 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 { // 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 index 551cd1d..1430c67 100644 --- a/FileOtterTests/FilePathTests.swift +++ b/FileOtterTests/FilePathTests.swift @@ -10,58 +10,61 @@ import XCTest final class FilePathTests: XCTestCase { var tempDir: URL! - + var originalWorkingDirectory: String! + override func setUpWithError() throws { + originalWorkingDirectory = FileManager.default.currentDirectoryPath tempDir = URL.temporaryDirectory .appendingPathComponent("FilePathTests-\(UUID().uuidString)") try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) } - + override func tearDownWithError() throws { + FileManager.default.changeCurrentDirectoryPath(originalWorkingDirectory) if FileManager.default.fileExists(atPath: tempDir.path) { try FileManager.default.removeItem(at: tempDir) } } - + // MARK: - basename Tests - + func testBasename() throws { let url1 = URL(fileURLWithPath: "/Users/sjs/file.txt") XCTAssertEqual(File.basename(url1), "file.txt") - + let url2 = URL(fileURLWithPath: "/Users/sjs/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: "/Users/sjs/file.txt") XCTAssertEqual(File.basename(url, suffix: ".txt"), "file") XCTAssertEqual(File.basename(url, suffix: ".rb"), "file.txt") - + let url2 = URL(fileURLWithPath: "/Users/sjs/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: "/Users/sjs/file.txt") XCTAssertEqual(File.basename(url, suffix: ".*"), "file") - + let url2 = URL(fileURLWithPath: "/Users/sjs/archive.tar.gz") XCTAssertEqual(File.basename(url2, suffix: ".*"), "archive.tar") - + let url3 = URL(fileURLWithPath: "/Users/sjs/noext") XCTAssertEqual(File.basename(url3, suffix: ".*"), "noext") } - + // MARK: - dirname Tests - + func testDirname() throws { let url1 = URL(fileURLWithPath: "/Users/sjs/file.txt") XCTAssertEqual(File.dirname(url1).path(), "/Users/sjs/") @@ -75,18 +78,18 @@ final class FilePathTests: XCTestCase { let url4 = URL(fileURLWithPath: "file.txt") XCTAssertEqual(File.dirname(url4).path(), "./") } - + func testDirnameWithLevel() throws { let url = URL(fileURLWithPath: "/Users/sjs/dir/file.txt") XCTAssertEqual(File.dirname(url, level: 1).path(), "/Users/sjs/dir/") XCTAssertEqual(File.dirname(url, level: 2).path(), "/Users/sjs/") XCTAssertEqual(File.dirname(url, level: 3).path(), "/Users/") XCTAssertEqual(File.dirname(url, level: 4).path(), "/") - XCTAssertEqual(File.dirname(url, level: 5).path(), "/") // Can't go beyond root + 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") @@ -94,41 +97,41 @@ final class FilePathTests: XCTestCase { 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: "/Users/sjs/.bashrc")), "") XCTAssertEqual(File.extname(URL(fileURLWithPath: "/Users/sjs/.config.bak")), ".bak") } - + // MARK: - split Tests - + func testSplit() throws { let (dir1, name1) = File.split(URL(fileURLWithPath: "/Users/sjs/file.txt")) XCTAssertEqual(dir1.path, "/Users/sjs") XCTAssertEqual(name1, "file.txt") - + let (dir2, name2) = File.split(URL(fileURLWithPath: "/file.txt")) XCTAssertEqual(dir2.path, "/") XCTAssertEqual(name2, "file.txt") - + let (dir3, name3) = File.split(URL(fileURLWithPath: "file.txt")) XCTAssertEqual(dir3.path(), "./") XCTAssertEqual(name3, "file.txt") - + let (dir4, name4) = File.split(URL(fileURLWithPath: "/Users/sjs/")) XCTAssertEqual(dir4.path, "/Users") XCTAssertEqual(name4, "sjs") - + // Root path edge case let (dir5, name5) = File.split(URL(fileURLWithPath: "/")) XCTAssertEqual(dir5.path, "/") XCTAssertEqual(name5, "") } - + // MARK: - join Tests - + func testJoin() throws { let u = URL(fileURLWithPath: "hello") XCTAssertEqual(File.join(u.path(), "world").path(), "hello/world") @@ -147,7 +150,7 @@ final class FilePathTests: XCTestCase { // Handles trailing slashes XCTAssertEqual(File.join("/usr/", "local/", "bin").path(), "/usr/local/bin/") } - + func testJoinWithArray() throws { let components = ["usr", "local", "bin"] XCTAssertEqual(File.join(components).path(), "usr/local/bin") @@ -158,49 +161,114 @@ final class FilePathTests: XCTestCase { let singleComponent = ["file.txt"] XCTAssertEqual(File.join(singleComponent).path(), "file.txt") } - + // MARK: - absolutePath Tests - + func testAbsolutePath() throws { - // TODO: Implement - // Converts relative to absolute + // Test already absolute path + let absoluteURL = URL(fileURLWithPath: "/usr/bin/swift") + XCTAssertEqual(File.absolutePath(absoluteURL).path, "/usr/bin/swift") + + // Test relative path - URL constructor will use current directory + FileManager.default.changeCurrentDirectoryPath(tempDir.path) + let relativeURL = URL(fileURLWithPath: "file.txt") + let absPath = File.absolutePath(relativeURL) + // The URL is already absolute at this point, we just normalize it + XCTAssertTrue(absPath.path.hasSuffix("file.txt")) + XCTAssertTrue(absPath.path.hasPrefix("/")) } - - func testAbsolutePathWithBase() throws { - // TODO: Implement + + func testAbsolutePathNormalization() throws { + // Test that .. and . are resolved + let pathWithDots = URL(fileURLWithPath: "/usr/../bin/./swift") + XCTAssertEqual(File.absolutePath(pathWithDots).path, "/bin/swift") + + // Test multiple .. segments + let pathWithMultipleDots = URL(fileURLWithPath: "/usr/local/../../bin") + XCTAssertEqual(File.absolutePath(pathWithMultipleDots).path, "/bin") + + // Test trailing slash removal + let pathWithTrailingSlash = URL(fileURLWithPath: "/usr/bin/") + XCTAssertEqual(File.absolutePath(pathWithTrailingSlash).path, "/usr/bin") } - + // MARK: - expandPath Tests - + func testExpandPath() throws { - // TODO: Implement - // File.expandPath("~") => home directory - // File.expandPath("~/Documents") => home/Documents + let homeDir = FileManager.default.homeDirectoryForCurrentUser + + // Test expanding ~ + let expanded1 = File.expandPath("~") + XCTAssertEqual(expanded1.path, homeDir.path) + + // Test expanding ~/Documents + let expanded2 = File.expandPath("~/Documents") + XCTAssertEqual(expanded2.path, homeDir.appendingPathComponent("Documents").path) + + // Test regular path (no expansion needed) + let expanded3 = File.expandPath("/usr/bin") + XCTAssertEqual(expanded3.path, "/usr/bin") } - - func testExpandPathWithRelative() throws { - // TODO: Implement - } - + // MARK: - realpath Tests - + func testRealpath() throws { - // TODO: Implement - // Resolves symlinks, all components must exist + // Create a real file + let fileURL = tempDir.appendingPathComponent("realfile.txt") + try "test content".write(to: fileURL, atomically: true, encoding: .utf8) + + // Test resolving a real file + let resolved = try File.realpath(fileURL) + XCTAssertEqual(resolved.path, fileURL.path) + + // Create a symlink + let symlinkURL = tempDir.appendingPathComponent("symlink.txt") + try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: fileURL) + + // Test resolving symlink + let resolvedSymlink = try File.realpath(symlinkURL) + XCTAssertEqual(resolvedSymlink.path, fileURL.path) } - + func testRealpathThrowsForNonExistent() throws { - // TODO: Implement + let nonExistentURL = tempDir.appendingPathComponent("nonexistent.txt") + + XCTAssertThrowsError(try File.realpath(nonExistentURL)) { error in + // Should throw file not found error + let nsError = error as NSError + XCTAssertEqual(nsError.domain, NSCocoaErrorDomain) + XCTAssertEqual(nsError.code, CocoaError.fileNoSuchFile.rawValue) + } } - + // MARK: - realdirpath Tests - + func testRealdirpath() throws { - // TODO: Implement - // Resolves symlinks, last component may not exist + // Create a directory + let dirURL = tempDir.appendingPathComponent("realdir") + try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) + + // Test resolving existing directory + let resolved = try File.realdirpath(dirURL) + XCTAssertEqual(resolved.path, dirURL.path) + + // Create a symlink to the directory + let symlinkDirURL = tempDir.appendingPathComponent("symlinkdir") + try FileManager.default.createSymbolicLink(at: symlinkDirURL, withDestinationURL: dirURL) + + // Test resolving symlinked directory + let resolvedSymlink = try File.realdirpath(symlinkDirURL) + XCTAssertEqual(resolvedSymlink.path, dirURL.path) } - + func testRealdirpathWithNonExistentLast() throws { - // TODO: Implement + // Create a real directory + let dirURL = tempDir.appendingPathComponent("realdir") + try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) + + // Test with non-existent file in existing directory + let nonExistentFile = dirURL.appendingPathComponent("future-file.txt") + let resolved = try File.realdirpath(nonExistentFile) + XCTAssertEqual(resolved.path, nonExistentFile.path) } } diff --git a/FileOtterTests/FilePermissionTests.swift b/FileOtterTests/FilePermissionTests.swift index 231ae2a..2e723f7 100644 --- a/FileOtterTests/FilePermissionTests.swift +++ b/FileOtterTests/FilePermissionTests.swift @@ -13,154 +13,231 @@ final class FilePermissionTests: XCTestCase { 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 + // Normal files should be readable + XCTAssertTrue(File.isReadable(testFile)) + + // System files are generally readable + XCTAssertTrue(File.isReadable(URL(fileURLWithPath: "/etc/hosts"))) + + // Non-existent files are not readable + let nonExistent = tempDir.appendingPathComponent("nonexistent") + XCTAssertFalse(File.isReadable(nonExistent)) } - + func testIsWritable() throws { - // TODO: Implement - // File.isWritable(url) returns true for writable files + // Files we created should be writable + XCTAssertTrue(File.isWritable(testFile)) + + // System files are generally not writable + XCTAssertFalse(File.isWritable(URL(fileURLWithPath: "/etc/hosts"))) + + // Non-existent files are not writable + let nonExistent = tempDir.appendingPathComponent("nonexistent") + XCTAssertFalse(File.isWritable(nonExistent)) } - + func testIsExecutable() throws { - // TODO: Implement - // File.isExecutable(url) returns true for executable files + // Make the script executable + try FileManager.default.setAttributes( + [.posixPermissions: 0o755], + ofItemAtPath: executableFile.path, + ) + XCTAssertTrue(File.isExecutable(executableFile)) + + // System executables + XCTAssertTrue(File.isExecutable(URL(fileURLWithPath: "/bin/ls"))) + XCTAssertTrue(File.isExecutable(URL(fileURLWithPath: "/usr/bin/swift"))) } - + func testIsExecutableForNonExecutable() throws { - // TODO: Implement - // File.isExecutable(url) returns false for non-executable + // Regular text files are not executable + XCTAssertFalse(File.isExecutable(testFile)) + XCTAssertFalse(File.isExecutable(readOnlyFile)) + + // Non-existent files are not executable + let nonExistent = tempDir.appendingPathComponent("nonexistent") + XCTAssertFalse(File.isExecutable(nonExistent)) } - + // MARK: - Ownership Tests - + func testIsOwned() throws { - // TODO: Implement - // File.isOwned(url) returns true for files owned by effective user + // Files we create should be owned by us + XCTAssertTrue(File.isOwned(testFile)) + XCTAssertTrue(File.isOwned(readOnlyFile)) + XCTAssertTrue(File.isOwned(executableFile)) + + // System files may not be owned by us + // This depends on the user running the test } - + func testIsGroupOwned() throws { - // TODO: Implement - // File.isGroupOwned(url) returns true for files owned by effective group + // Files we create should be owned by our effective group + XCTAssertTrue(File.isGroupOwned(testFile)) + XCTAssertTrue(File.isGroupOwned(readOnlyFile)) + XCTAssertTrue(File.isGroupOwned(executableFile)) } - + // MARK: - World Permission Tests - + func testIsWorldReadable() throws { - // TODO: Implement - // File.isWorldReadable(url) returns permission bits if world readable + // Make file world readable + try FileManager.default.setAttributes( + [.posixPermissions: 0o644], + ofItemAtPath: testFile.path, + ) + + let perms = File.isWorldReadable(testFile) + XCTAssertNotNil(perms) + if let perms { + XCTAssertEqual(perms & 0o004, 0o004) // Check world read bit + } } - + func testIsWorldReadableForPrivate() throws { - // TODO: Implement - // File.isWorldReadable(url) returns nil if not world readable + // Make file not world readable + try FileManager.default.setAttributes( + [.posixPermissions: 0o640], + ofItemAtPath: readOnlyFile.path, + ) + + XCTAssertNil(File.isWorldReadable(readOnlyFile)) } - + func testIsWorldWritable() throws { - // TODO: Implement - // File.isWorldWritable(url) returns permission bits if world writable + // Make file world writable (dangerous in practice!) + try FileManager.default.setAttributes( + [.posixPermissions: 0o666], + ofItemAtPath: testFile.path, + ) + + let perms = File.isWorldWritable(testFile) + XCTAssertNotNil(perms) + if let perms { + XCTAssertEqual(perms & 0o002, 0o002) // Check world write bit + } } - + func testIsWorldWritableForProtected() throws { - // TODO: Implement - // File.isWorldWritable(url) returns nil if not world writable + // Make file not world writable + try FileManager.default.setAttributes( + [.posixPermissions: 0o644], + ofItemAtPath: readOnlyFile.path, + ) + + XCTAssertNil(File.isWorldWritable(readOnlyFile)) } - + // MARK: - Special Bit Tests - + func testIsSetuid() throws { - // TODO: Implement - // File.isSetuid(url) returns true if setuid bit is set + // Setuid is rarely used on regular files + // Most files should not have setuid + XCTAssertFalse(File.isSetuid(testFile)) + + // /usr/bin/sudo typically has setuid (if it exists) + let sudo = URL(fileURLWithPath: "/usr/bin/sudo") + if FileManager.default.fileExists(atPath: sudo.path) { + // This might be true on some systems + _ = File.isSetuid(sudo) + } } - + func testIsSetgid() throws { - // TODO: Implement - // File.isSetgid(url) returns true if setgid bit is set + // Setgid is rarely used on regular files + XCTAssertFalse(File.isSetgid(testFile)) } - + func testIsSticky() throws { - // TODO: Implement - // File.isSticky(url) returns true if sticky bit is set + // Sticky bit is typically set on /tmp + let tmpDir = URL(fileURLWithPath: "/tmp") + if FileManager.default.fileExists(atPath: tmpDir.path) { + // /tmp usually has sticky bit + _ = File.isSticky(tmpDir) + } + + // Regular files should not have sticky bit + XCTAssertFalse(File.isSticky(testFile)) } - + // 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 index f10f23e..c149be4 100644 --- a/FileOtterTests/FileTypeTests.swift +++ b/FileOtterTests/FileTypeTests.swift @@ -12,77 +12,77 @@ 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)) @@ -90,119 +90,172 @@ final class FileTypeTests: XCTestCase { 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) + // Create symlink to test file + let symlinkURL = tempDir.appendingPathComponent("symlink.txt") + try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: testFile) + XCTAssertTrue(File.isSymlink(symlinkURL)) + + // Create symlink to directory + let dirSymlinkURL = tempDir.appendingPathComponent("dirlink") + try FileManager.default.createSymbolicLink(at: dirSymlinkURL, withDestinationURL: testDir) + XCTAssertTrue(File.isSymlink(dirSymlinkURL)) } - + func testIsSymlinkForRegularFile() throws { - // TODO: Implement - // File.isSymlink(url) returns false for regular files + // Regular files are not symlinks + XCTAssertFalse(File.isSymlink(testFile)) + + // Directories are not symlinks + XCTAssertFalse(File.isSymlink(testDir)) + + // Non-existent paths are not symlinks + let nonExistent = tempDir.appendingPathComponent("nonexistent") + XCTAssertFalse(File.isSymlink(nonExistent)) } - + func testIsBlockDevice() throws { - // TODO: Implement - // File.isBlockDevice(url) - may need special test file + // Block devices are rare on macOS, but /dev/disk* exists + // This test might fail in sandboxed environments + if FileManager.default.fileExists(atPath: "/dev/disk0") { + XCTAssertTrue(File.isBlockDevice(URL(fileURLWithPath: "/dev/disk0"))) + } + + // Regular files are not block devices + XCTAssertFalse(File.isBlockDevice(testFile)) + XCTAssertFalse(File.isBlockDevice(testDir)) } - + func testIsCharDevice() throws { - // TODO: Implement - // File.isCharDevice(url) - test with /dev/null or similar + // /dev/null is always a character device + let devNull = URL(fileURLWithPath: "/dev/null") + XCTAssertTrue(File.isCharDevice(devNull)) + + // /dev/random is also a character device + let devRandom = URL(fileURLWithPath: "/dev/random") + if FileManager.default.fileExists(atPath: devRandom.path) { + XCTAssertTrue(File.isCharDevice(devRandom)) + } + + // Regular files are not character devices + XCTAssertFalse(File.isCharDevice(testFile)) + XCTAssertFalse(File.isCharDevice(testDir)) } - + func testIsPipe() throws { - // TODO: Implement - // File.isPipe(url) - create FIFO and test + // Creating FIFOs requires mkfifo system call + // Skip this test for now as it requires additional implementation + // Regular files are not pipes + XCTAssertFalse(File.isPipe(testFile)) + XCTAssertFalse(File.isPipe(testDir)) } - + func testIsSocket() throws { - // TODO: Implement - // File.isSocket(url) - create socket and test + // Unix domain sockets are rare and hard to create in tests + // Regular files are not sockets + XCTAssertFalse(File.isSocket(testFile)) + XCTAssertFalse(File.isSocket(testDir)) } - + // MARK: - Empty/Zero Tests - + func testIsEmpty() throws { // 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 { // 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 { let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt") XCTAssertThrowsError(try File.isEmpty(nonExistent)) } - + func testIsZero() throws { // 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 - + func testFtypeForFile() throws { - // TODO: Implement - // File.ftype(url) returns .file + XCTAssertEqual(File.ftype(testFile), .file) + + // Create another file to test + let anotherFile = tempDir.appendingPathComponent("another.dat") + try Data().write(to: anotherFile) + XCTAssertEqual(File.ftype(anotherFile), .file) } - + func testFtypeForDirectory() throws { - // TODO: Implement - // File.ftype(url) returns .directory + XCTAssertEqual(File.ftype(testDir), .directory) + XCTAssertEqual(File.ftype(tempDir), .directory) + XCTAssertEqual(File.ftype(URL(fileURLWithPath: "/")), .directory) } - + func testFtypeForSymlink() throws { - // TODO: Implement - // File.ftype(url) returns .link + let symlinkURL = tempDir.appendingPathComponent("link.txt") + try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: testFile) + XCTAssertEqual(File.ftype(symlinkURL), .link) + + // Symlink to directory + let dirSymlinkURL = tempDir.appendingPathComponent("dirlink") + try FileManager.default.createSymbolicLink(at: dirSymlinkURL, withDestinationURL: testDir) + XCTAssertEqual(File.ftype(dirSymlinkURL), .link) } - + func testFtypeForCharDevice() throws { - // TODO: Implement - // File.ftype(url) returns .characterSpecial + // /dev/null is a character device + let devNull = URL(fileURLWithPath: "/dev/null") + XCTAssertEqual(File.ftype(devNull), .characterSpecial) } - + func testFtypeForBlockDevice() throws { - // TODO: Implement - // File.ftype(url) returns .blockSpecial + // Block devices are rare on macOS + // This test might fail in sandboxed environments + if FileManager.default.fileExists(atPath: "/dev/disk0") { + XCTAssertEqual(File.ftype(URL(fileURLWithPath: "/dev/disk0")), .blockSpecial) + } } - + func testFtypeForFifo() throws { - // TODO: Implement - // File.ftype(url) returns .fifo + // FIFOs require special creation + // Skip for now } - + func testFtypeForSocket() throws { - // TODO: Implement - // File.ftype(url) returns .socket + // Sockets require special creation + // Skip for now } - + func testFtypeForUnknown() throws { - // TODO: Implement - // File.ftype(url) returns .unknown for unrecognized types + // Non-existent files return unknown + let nonExistent = tempDir.appendingPathComponent("nonexistent") + XCTAssertEqual(File.ftype(nonExistent), .unknown) } -} \ No newline at end of file +}