WIP: Implement more of File

This commit is contained in:
Sami Samhuri 2025-08-26 08:35:49 -07:00
parent f24ed714e6
commit 4529a7e700
No known key found for this signature in database
4 changed files with 207 additions and 95 deletions

View file

@ -3,18 +3,12 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 56; objectVersion = 70;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
7B1B71E12E52784D008EDC0E /* Glob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B71E02E52784D008EDC0E /* Glob.swift */; };
7B5064B92BD9F236009CEFF9 /* FileOtter.docc in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064B82BD9F236009CEFF9 /* FileOtter.docc */; };
7B5064BF2BD9F236009CEFF9 /* FileOtter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */; }; 7B5064BF2BD9F236009CEFF9 /* FileOtter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */; };
7B5064C42BD9F236009CEFF9 /* FileOtterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */; };
7B5064C52BD9F236009CEFF9 /* FileOtter.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B5064B72BD9F236009CEFF9 /* FileOtter.h */; settings = {ATTRIBUTES = (Public, ); }; };
7B5064CF2BD9F2C0009CEFF9 /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 7B5064CE2BD9F2C0009CEFF9 /* Readme.md */; }; 7B5064CF2BD9F2C0009CEFF9 /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 7B5064CE2BD9F2C0009CEFF9 /* Readme.md */; };
7B5064D12BD9F322009CEFF9 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064D02BD9F322009CEFF9 /* File.swift */; };
7B5064D32BD9F339009CEFF9 /* Dir.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064D22BD9F339009CEFF9 /* Dir.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -28,17 +22,26 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
7B1B71E02E52784D008EDC0E /* Glob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glob.swift; sourceTree = "<group>"; };
7B5064B42BD9F236009CEFF9 /* FileOtter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileOtter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileOtter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7B5064B72BD9F236009CEFF9 /* FileOtter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileOtter.h; sourceTree = "<group>"; };
7B5064B82BD9F236009CEFF9 /* FileOtter.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = FileOtter.docc; sourceTree = "<group>"; };
7B5064BE2BD9F236009CEFF9 /* FileOtterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FileOtterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7B5064BE2BD9F236009CEFF9 /* FileOtterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FileOtterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOtterTests.swift; sourceTree = "<group>"; };
7B5064CE2BD9F2C0009CEFF9 /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = "<group>"; }; 7B5064CE2BD9F2C0009CEFF9 /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = "<group>"; };
7B5064D02BD9F322009CEFF9 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
7B5064D22BD9F339009CEFF9 /* Dir.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dir.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7B1B74752E5E097B008EDC0E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
publicHeaders = (
FileOtter.h,
);
target = 7B5064B32BD9F235009CEFF9 /* FileOtter */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7B1B74612E5E0978008EDC0E /* FileOtterTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FileOtterTests; sourceTree = "<group>"; };
7B1B746F2E5E097B008EDC0E /* FileOtter */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7B1B74752E5E097B008EDC0E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = FileOtter; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
7B5064B12BD9F235009CEFF9 /* Frameworks */ = { 7B5064B12BD9F235009CEFF9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
@ -62,8 +65,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7B5064CE2BD9F2C0009CEFF9 /* Readme.md */, 7B5064CE2BD9F2C0009CEFF9 /* Readme.md */,
7B5064B62BD9F236009CEFF9 /* FileOtter */, 7B1B746F2E5E097B008EDC0E /* FileOtter */,
7B5064C22BD9F236009CEFF9 /* FileOtterTests */, 7B1B74612E5E0978008EDC0E /* FileOtterTests */,
7B5064B52BD9F236009CEFF9 /* Products */, 7B5064B52BD9F236009CEFF9 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@ -77,26 +80,6 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7B5064B62BD9F236009CEFF9 /* FileOtter */ = {
isa = PBXGroup;
children = (
7B5064B72BD9F236009CEFF9 /* FileOtter.h */,
7B5064B82BD9F236009CEFF9 /* FileOtter.docc */,
7B5064D02BD9F322009CEFF9 /* File.swift */,
7B5064D22BD9F339009CEFF9 /* Dir.swift */,
7B1B71E02E52784D008EDC0E /* Glob.swift */,
);
path = FileOtter;
sourceTree = "<group>";
};
7B5064C22BD9F236009CEFF9 /* FileOtterTests */ = {
isa = PBXGroup;
children = (
7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */,
);
path = FileOtterTests;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
@ -104,7 +87,6 @@
isa = PBXHeadersBuildPhase; isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7B5064C52BD9F236009CEFF9 /* FileOtter.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -124,6 +106,9 @@
); );
dependencies = ( dependencies = (
); );
fileSystemSynchronizedGroups = (
7B1B746F2E5E097B008EDC0E /* FileOtter */,
);
name = FileOtter; name = FileOtter;
productName = FileOtter; productName = FileOtter;
productReference = 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */; productReference = 7B5064B42BD9F236009CEFF9 /* FileOtter.framework */;
@ -142,6 +127,9 @@
dependencies = ( dependencies = (
7B5064C12BD9F236009CEFF9 /* PBXTargetDependency */, 7B5064C12BD9F236009CEFF9 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = (
7B1B74612E5E0978008EDC0E /* FileOtterTests */,
);
name = FileOtterTests; name = FileOtterTests;
productName = FileOtterTests; productName = FileOtterTests;
productReference = 7B5064BE2BD9F236009CEFF9 /* FileOtterTests.xctest */; productReference = 7B5064BE2BD9F236009CEFF9 /* FileOtterTests.xctest */;
@ -207,10 +195,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7B5064B92BD9F236009CEFF9 /* FileOtter.docc in Sources */,
7B1B71E12E52784D008EDC0E /* Glob.swift in Sources */,
7B5064D12BD9F322009CEFF9 /* File.swift in Sources */,
7B5064D32BD9F339009CEFF9 /* Dir.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -218,7 +202,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7B5064C42BD9F236009CEFF9 /* FileOtterTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -439,7 +422,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = X45WPY5JFZ; DEVELOPMENT_TEAM = X45WPY5JFZ;
@ -461,7 +443,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = X45WPY5JFZ; DEVELOPMENT_TEAM = X45WPY5JFZ;

View file

@ -149,7 +149,7 @@ public extension File {
static func dirname(_ url: URL, level: Int = 1) -> URL { static func dirname(_ url: URL, level: Int = 1) -> URL {
var result = url var result = url
for _ in 0..<level { for _ in 0..<level where result.path != "/" {
result = result.deletingLastPathComponent() result = result.deletingLastPathComponent()
} }
return result return result
@ -161,15 +161,42 @@ public extension File {
} }
static func split(_ url: URL) -> (dir: URL, name: String) { static func split(_ url: URL) -> (dir: URL, name: String) {
fatalError("Not implemented") 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 { static func join(_ components: String...) -> URL {
fatalError("Not implemented") join(components)
} }
static func join(_ components: [String]) -> URL { static func join(_ components: [String]) -> URL {
fatalError("Not implemented") // 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
let trimmed = component.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if !trimmed.isEmpty {
result.appendPathComponent(trimmed)
}
}
return result
} }
static func absolutePath(_ url: URL, relativeTo base: URL? = nil) -> URL { static func absolutePath(_ url: URL, relativeTo base: URL? = nil) -> URL {
@ -193,23 +220,55 @@ public extension File {
public extension File { public extension File {
static func atime(_ url: URL) throws -> Date { static func atime(_ url: URL) throws -> Date {
fatalError("Not implemented") // 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 { static func mtime(_ url: URL) throws -> Date {
fatalError("Not implemented") let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let modDate = attributes[.modificationDate] as? Date else {
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
return modDate
} }
static func ctime(_ url: URL) throws -> Date { static func ctime(_ url: URL) throws -> Date {
fatalError("Not implemented") // 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 { static func birthtime(_ url: URL) throws -> Date {
fatalError("Not implemented") let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let creationDate = attributes[.creationDate] as? Date else {
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
return creationDate
} }
static func size(_ url: URL) throws -> Int { static func size(_ url: URL) throws -> Int {
fatalError("Not implemented") let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let fileSize = attributes[.size] as? NSNumber else {
throw CocoaError(.fileReadUnknown, userInfo: [NSFilePathErrorKey: url.path])
}
return fileSize.intValue
} }
static func stat(_ url: URL) throws -> FileStat { static func stat(_ url: URL) throws -> FileStat {

View file

@ -30,23 +30,44 @@ final class FileInfoTests: XCTestCase {
// MARK: - Time-based Tests // MARK: - Time-based Tests
func testAtime() throws { func testAtime() throws {
// TODO: Implement let atime = try File.atime(testFile)
// File.atime(url) returns last access time XCTAssertNotNil(atime)
// Access time should be recent (within last hour)
XCTAssertLessThan(Date().timeIntervalSince(atime), 3600)
} }
func testMtime() throws { func testMtime() throws {
// TODO: Implement // Get initial mtime
// File.mtime(url) returns last modification time 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 { func testCtime() throws {
// TODO: Implement let ctime = try File.ctime(testFile)
// File.ctime(url) returns last status change time XCTAssertNotNil(ctime)
// Status change time should be recent
XCTAssertLessThan(Date().timeIntervalSince(ctime), 3600)
} }
func testBirthtime() throws { func testBirthtime() throws {
// TODO: Implement // Create a new file
// File.birthtime(url) returns creation time let newFile = tempDir.appendingPathComponent("birthtime-test.txt")
let beforeCreation = Date()
Thread.sleep(forTimeInterval: 0.01)
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 { func testBirthtimeThrowsOnUnsupportedPlatform() throws {
@ -56,16 +77,28 @@ final class FileInfoTests: XCTestCase {
// MARK: - Size Tests // MARK: - Size Tests
func testSize() throws { func testSize() throws {
// TODO: Implement // Test with known content
// File.size(url) returns file size in bytes 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)
try largeContent.write(to: largeFile, atomically: true, encoding: .utf8)
let largeExpectedSize = largeContent.data(using: .utf8)!.count
XCTAssertEqual(try File.size(largeFile), largeExpectedSize)
} }
func testSizeThrowsForNonExistent() throws { func testSizeThrowsForNonExistent() throws {
// TODO: Implement let nonExistent = tempDir.appendingPathComponent("no-such-file.txt")
XCTAssertThrowsError(try File.size(nonExistent))
} }
func testSizeForEmptyFile() throws { func testSizeForEmptyFile() throws {
// TODO: Implement let emptyFile = tempDir.appendingPathComponent("empty.txt")
try "".write(to: emptyFile, atomically: true, encoding: .utf8)
XCTAssertEqual(try File.size(emptyFile), 0)
} }
// MARK: - Stat Tests // MARK: - Stat Tests

View file

@ -26,10 +26,10 @@ final class FilePathTests: XCTestCase {
// MARK: - basename Tests // MARK: - basename Tests
func testBasename() throws { func testBasename() throws {
let url1 = URL(fileURLWithPath: "/home/user/file.txt") let url1 = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.basename(url1), "file.txt") XCTAssertEqual(File.basename(url1), "file.txt")
let url2 = URL(fileURLWithPath: "/home/user/dir/") let url2 = URL(fileURLWithPath: "/Users/sjs/dir/")
XCTAssertEqual(File.basename(url2), "dir") XCTAssertEqual(File.basename(url2), "dir")
let url3 = URL(fileURLWithPath: "/") let url3 = URL(fileURLWithPath: "/")
@ -40,49 +40,49 @@ final class FilePathTests: XCTestCase {
} }
func testBasenameWithSuffix() throws { func testBasenameWithSuffix() throws {
let url = URL(fileURLWithPath: "/home/user/file.txt") let url = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.basename(url, suffix: ".txt"), "file") XCTAssertEqual(File.basename(url, suffix: ".txt"), "file")
XCTAssertEqual(File.basename(url, suffix: ".rb"), "file.txt") XCTAssertEqual(File.basename(url, suffix: ".rb"), "file.txt")
let url2 = URL(fileURLWithPath: "/home/user/archive.tar.gz") let url2 = URL(fileURLWithPath: "/Users/sjs/archive.tar.gz")
XCTAssertEqual(File.basename(url2, suffix: ".gz"), "archive.tar") XCTAssertEqual(File.basename(url2, suffix: ".gz"), "archive.tar")
XCTAssertEqual(File.basename(url2, suffix: ".tar.gz"), "archive") XCTAssertEqual(File.basename(url2, suffix: ".tar.gz"), "archive")
} }
func testBasenameWithWildcardSuffix() throws { func testBasenameWithWildcardSuffix() throws {
let url = URL(fileURLWithPath: "/home/user/file.txt") let url = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.basename(url, suffix: ".*"), "file") XCTAssertEqual(File.basename(url, suffix: ".*"), "file")
let url2 = URL(fileURLWithPath: "/home/user/archive.tar.gz") let url2 = URL(fileURLWithPath: "/Users/sjs/archive.tar.gz")
XCTAssertEqual(File.basename(url2, suffix: ".*"), "archive.tar") XCTAssertEqual(File.basename(url2, suffix: ".*"), "archive.tar")
let url3 = URL(fileURLWithPath: "/home/user/noext") let url3 = URL(fileURLWithPath: "/Users/sjs/noext")
XCTAssertEqual(File.basename(url3, suffix: ".*"), "noext") XCTAssertEqual(File.basename(url3, suffix: ".*"), "noext")
} }
// MARK: - dirname Tests // MARK: - dirname Tests
func testDirname() throws { func testDirname() throws {
let url1 = URL(fileURLWithPath: "/home/user/file.txt") let url1 = URL(fileURLWithPath: "/Users/sjs/file.txt")
XCTAssertEqual(File.dirname(url1).path, "/home/user") XCTAssertEqual(File.dirname(url1).path(), "/Users/sjs/")
let url2 = URL(fileURLWithPath: "/home/user/dir/") let url2 = URL(fileURLWithPath: "/Users/sjs/dir/")
XCTAssertEqual(File.dirname(url2).path, "/home/user") XCTAssertEqual(File.dirname(url2).path(), "/Users/sjs/")
let url3 = URL(fileURLWithPath: "/file.txt") let url3 = URL(fileURLWithPath: "/file.txt")
XCTAssertEqual(File.dirname(url3).path, "/") XCTAssertEqual(File.dirname(url3).path(), "/")
let url4 = URL(fileURLWithPath: "file.txt") let url4 = URL(fileURLWithPath: "file.txt")
XCTAssertEqual(File.dirname(url4).path, ".") XCTAssertEqual(File.dirname(url4).path(), "./")
} }
func testDirnameWithLevel() throws { func testDirnameWithLevel() throws {
let url = URL(fileURLWithPath: "/home/user/dir/file.txt") let url = URL(fileURLWithPath: "/Users/sjs/dir/file.txt")
XCTAssertEqual(File.dirname(url, level: 1).path, "/home/user/dir") XCTAssertEqual(File.dirname(url, level: 1).path(), "/Users/sjs/dir/")
XCTAssertEqual(File.dirname(url, level: 2).path, "/home/user") XCTAssertEqual(File.dirname(url, level: 2).path(), "/Users/sjs/")
XCTAssertEqual(File.dirname(url, level: 3).path, "/home") XCTAssertEqual(File.dirname(url, level: 3).path(), "/Users/")
XCTAssertEqual(File.dirname(url, level: 4).path, "/") 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 // MARK: - extname Tests
@ -98,26 +98,65 @@ final class FilePathTests: XCTestCase {
func testExtnameWithDotfile() throws { func testExtnameWithDotfile() throws {
XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile")), "") XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile")), "")
XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile.sh")), ".sh") XCTAssertEqual(File.extname(URL(fileURLWithPath: ".profile.sh")), ".sh")
XCTAssertEqual(File.extname(URL(fileURLWithPath: "/home/user/.bashrc")), "") XCTAssertEqual(File.extname(URL(fileURLWithPath: "/Users/sjs/.bashrc")), "")
XCTAssertEqual(File.extname(URL(fileURLWithPath: "/home/user/.config.bak")), ".bak") XCTAssertEqual(File.extname(URL(fileURLWithPath: "/Users/sjs/.config.bak")), ".bak")
} }
// MARK: - split Tests // MARK: - split Tests
func testSplit() throws { func testSplit() throws {
// TODO: Implement let (dir1, name1) = File.split(URL(fileURLWithPath: "/Users/sjs/file.txt"))
// File.split("/home/user/file.txt") => ("/home/user", "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 // MARK: - join Tests
func testJoin() throws { func testJoin() throws {
// TODO: Implement let u = URL(fileURLWithPath: "hello")
// File.join("usr", "mail", "gumby") => "usr/mail/gumby" XCTAssertEqual(File.join(u.path(), "world").path(), "hello/world")
XCTAssertEqual(File.join("usr", "mail", "gumby").path(), "usr/mail/gumby")
XCTAssertEqual(File.join("/usr", "mail", "gumby").path(), "/usr/mail/gumby")
XCTAssertEqual(File.join("/", "usr", "bin").path(), "/usr/bin/")
// Single component
XCTAssertEqual(File.join("file.txt").path(), "file.txt")
XCTAssertEqual(File.join("/file.txt").path(), "/file.txt")
// Empty components are ignored
XCTAssertEqual(File.join("usr", "", "bin").path(), "usr/bin")
// Handles trailing slashes
XCTAssertEqual(File.join("/usr/", "local/", "bin").path(), "/usr/local/bin/")
} }
func testJoinWithArray() throws { func testJoinWithArray() throws {
// TODO: Implement let components = ["usr", "local", "bin"]
XCTAssertEqual(File.join(components).path(), "usr/local/bin")
let absoluteComponents = ["/usr", "local", "bin"]
XCTAssertEqual(File.join(absoluteComponents).path(), "/usr/local/bin/")
let singleComponent = ["file.txt"]
XCTAssertEqual(File.join(singleComponent).path(), "file.txt")
} }
// MARK: - absolutePath Tests // MARK: - absolutePath Tests