mirror of
https://github.com/samsonjs/FileOtter.git
synced 2026-03-25 08:25:49 +00:00
WIP: Implement more of File
This commit is contained in:
parent
ab21045498
commit
993cc73ec0
11 changed files with 925 additions and 543 deletions
|
|
@ -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<T>(
|
||||
prefix: String = "d",
|
||||
suffix: String = "",
|
||||
_ block: (URL) throws -> T
|
||||
_ block: (URL) throws -> T,
|
||||
) throws -> T {
|
||||
let tmpDir = try mktmpdir(prefix: prefix, suffix: suffix)
|
||||
defer {
|
||||
|
|
|
|||
|
|
@ -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<T>(url: URL, mode: Mode = .read, permissions: Int = 0o666, _ block: (File) throws -> T) rethrows -> T {
|
||||
public static func open<T>(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 {
|
||||
"<File:\(url.path) mode:\(mode)>"
|
||||
}
|
||||
|
|
@ -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..<level where result.path != "/" {
|
||||
for _ in 0 ..< level where result.path != "/" {
|
||||
result = result.deletingLastPathComponent()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
static func extname(_ url: URL) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func globstar(_ pattern: String, base: URL? = nil) -> [String] {
|
|||
|
||||
var results: [String] = []
|
||||
var seenDirs = Set<String>() // 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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue