mirror of
https://github.com/samsonjs/FileOtter.git
synced 2026-03-25 08:25:49 +00:00
Flesh out Dir a lot more
This commit is contained in:
parent
c74842ef76
commit
f85da3e7b5
5 changed files with 695 additions and 72 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
build
|
||||
xcuserdata
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
7B5064C42BD9F236009CEFF9 /* FileOtterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5064C32BD9F236009CEFF9 /* FileOtterTests.swift */; };
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
/* End PBXContainerItemProxy 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; };
|
||||
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>"; };
|
||||
|
|
@ -82,6 +84,7 @@
|
|||
7B5064B82BD9F236009CEFF9 /* FileOtter.docc */,
|
||||
7B5064D02BD9F322009CEFF9 /* File.swift */,
|
||||
7B5064D22BD9F339009CEFF9 /* Dir.swift */,
|
||||
7B1B71E02E52784D008EDC0E /* Glob.swift */,
|
||||
);
|
||||
path = FileOtter;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -205,6 +208,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7B5064B92BD9F236009CEFF9 /* FileOtter.docc in Sources */,
|
||||
7B1B71E12E52784D008EDC0E /* Glob.swift in Sources */,
|
||||
7B5064D12BD9F322009CEFF9 /* File.swift in Sources */,
|
||||
7B5064D32BD9F339009CEFF9 /* Dir.swift in Sources */,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct Dir: Equatable, Hashable, RandomAccessCollection {
|
||||
public struct Dir: Equatable, Hashable, RandomAccessCollection, CustomStringConvertible, CustomDebugStringConvertible {
|
||||
public let startIndex: Int
|
||||
|
||||
public let endIndex: Int
|
||||
|
|
@ -15,7 +15,7 @@ public struct Dir: Equatable, Hashable, RandomAccessCollection {
|
|||
public let url: URL
|
||||
|
||||
public init(url: URL) throws {
|
||||
self.init(url: url, children: try Dir.children(url))
|
||||
try self.init(url: url, children: Dir.children(url))
|
||||
}
|
||||
|
||||
private let children: [URL]
|
||||
|
|
@ -26,96 +26,124 @@ public struct Dir: Equatable, Hashable, RandomAccessCollection {
|
|||
startIndex = children.startIndex
|
||||
endIndex = children.endIndex
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
url.path
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"<Dir:\(url.path)>"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Well-known Directories
|
||||
extension Dir {
|
||||
public static var caches: URL {
|
||||
|
||||
public extension Dir {
|
||||
static var caches: URL {
|
||||
URL.cachesDirectory
|
||||
}
|
||||
|
||||
public static var current: URL {
|
||||
URL.currentDirectory()
|
||||
}
|
||||
|
||||
public static var documents: URL {
|
||||
static var documents: URL {
|
||||
URL.documentsDirectory
|
||||
}
|
||||
|
||||
public static var home: URL {
|
||||
static var home: URL {
|
||||
URL.homeDirectory
|
||||
}
|
||||
|
||||
public static var library: URL {
|
||||
static var library: URL {
|
||||
URL.libraryDirectory
|
||||
}
|
||||
|
||||
public static var pwd: URL {
|
||||
.currentDirectory()
|
||||
static var pwd: URL {
|
||||
URL.currentDirectory()
|
||||
}
|
||||
|
||||
public static var getwd: URL {
|
||||
.currentDirectory()
|
||||
static var tmp: URL {
|
||||
URL.temporaryDirectory
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mutations
|
||||
extension Dir {
|
||||
@discardableResult
|
||||
public static func chdir(_ url: URL) -> Bool {
|
||||
FileManager.default.changeCurrentDirectoryPath(url.path)
|
||||
|
||||
public extension Dir {
|
||||
static func chdir(_ url: URL) throws {
|
||||
guard FileManager.default.changeCurrentDirectoryPath(url.path) else {
|
||||
throw CocoaError(.fileNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func chdir<T>(_ url: URL, block: (URL) -> T) -> T {
|
||||
static func chdir<T>(_ url: URL, block: (URL) throws -> T) rethrows -> T {
|
||||
let previousDir = pwd
|
||||
FileManager.default.changeCurrentDirectoryPath(url.path)
|
||||
defer {
|
||||
FileManager.default.changeCurrentDirectoryPath(previousDir.path)
|
||||
}
|
||||
return block(url)
|
||||
return try block(url)
|
||||
}
|
||||
|
||||
static func unlink(_ url: URL) throws {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
|
||||
static func rmdir(_ url: URL) throws {
|
||||
try unlink(url)
|
||||
}
|
||||
|
||||
static func mkdir(_ url: URL, permissions: Int = 0o755) throws {
|
||||
let attributes: [FileAttributeKey: Any] = [
|
||||
.posixPermissions: permissions,
|
||||
]
|
||||
try FileManager.default.createDirectory(
|
||||
at: url,
|
||||
withIntermediateDirectories: false,
|
||||
attributes: attributes
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func unlink(_ url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
static func mktmpdir(prefix: String = "d", suffix: String = "") throws -> URL {
|
||||
let tmpBase = URL.temporaryDirectory
|
||||
let dirName = suffix.isEmpty ? "\(prefix)-\(UUID().uuidString)" : "\(prefix)-\(UUID().uuidString)-\(suffix)"
|
||||
let tmpDir = tmpBase.appendingPathComponent(dirName)
|
||||
try FileManager.default.createDirectory(
|
||||
at: tmpDir,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: [.posixPermissions: 0o700]
|
||||
)
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func mktmpdir<T>(
|
||||
prefix: String = "d",
|
||||
suffix: String = "",
|
||||
_ block: (URL) throws -> T
|
||||
) throws -> T {
|
||||
let tmpDir = try mktmpdir(prefix: prefix, suffix: suffix)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func rmdir(_ url: URL) -> Bool {
|
||||
unlink(url)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func delete(_ url: URL) -> Bool {
|
||||
unlink(url)
|
||||
return try block(tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reading Contents
|
||||
extension Dir {
|
||||
public static func children(_ url: URL) throws -> [URL] {
|
||||
|
||||
public extension Dir {
|
||||
static func children(_ url: URL) throws -> [URL] {
|
||||
try FileManager.default
|
||||
.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
||||
}
|
||||
|
||||
public static func entries(_ url: URL) throws -> [String] {
|
||||
#warning("TODO: implement this ... maybe, it's dumb")
|
||||
return []
|
||||
}
|
||||
|
||||
public static func exists(_ url: URL) throws -> Bool {
|
||||
static func exists(_ url: URL) throws -> Bool {
|
||||
var isDirectory: ObjCBool = false
|
||||
let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
|
||||
return exists && isDirectory.boolValue
|
||||
}
|
||||
|
||||
public static func isEmpty(_ url: URL) throws -> Bool {
|
||||
static func isEmpty(_ url: URL) throws -> Bool {
|
||||
try FileManager.default
|
||||
.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
||||
.isEmpty
|
||||
|
|
@ -123,28 +151,35 @@ extension Dir {
|
|||
}
|
||||
|
||||
// MARK: - Globbing
|
||||
extension Dir {
|
||||
public static func glob(base: URL? = nil, _ patterns: String...) -> [URL] {
|
||||
|
||||
public extension Dir {
|
||||
static func glob(base: URL? = nil, _ patterns: String...) -> [URL] {
|
||||
_glob(base: base, patterns: patterns)
|
||||
}
|
||||
|
||||
public static subscript(base: URL? = nil, _ patterns: String...) -> [URL] {
|
||||
static subscript(base: URL?, _ patterns: String...) -> [URL] {
|
||||
_glob(base: base, patterns: patterns)
|
||||
}
|
||||
|
||||
static subscript(_ patterns: String...) -> [URL] {
|
||||
_glob(base: nil, patterns: Array(patterns))
|
||||
}
|
||||
|
||||
private static func _glob(base: URL?, patterns: [String]) -> [URL] {
|
||||
#warning("TODO: implement me")
|
||||
return []
|
||||
patterns.flatMap { pattern in
|
||||
globstar(pattern, base: base)
|
||||
}.map { URL(fileURLWithPath: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RandomAccessCollection
|
||||
extension Dir {
|
||||
public func makeIterator() -> any IteratorProtocol<URL> {
|
||||
|
||||
public extension Dir {
|
||||
func makeIterator() -> any IteratorProtocol<URL> {
|
||||
children.makeIterator()
|
||||
}
|
||||
|
||||
public subscript(position: Int) -> URL {
|
||||
subscript(position: Int) -> URL {
|
||||
children[position]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
121
FileOtter/Glob.swift
Normal file
121
FileOtter/Glob.swift
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// Glob.swift
|
||||
// FileOtter
|
||||
//
|
||||
// Created by Sami Samhuri on 2025-08-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(Linux)
|
||||
import Glibc
|
||||
#else
|
||||
import Darwin
|
||||
#endif
|
||||
|
||||
/// Expand a glob pattern supporting ** (recursive), *, ?, and [].
|
||||
/// Examples:
|
||||
/// "src/**/*.swift"
|
||||
/// "/var/log/**/app*.log"
|
||||
func globstar(_ pattern: String, base: URL? = nil) -> [URL] {
|
||||
// Normalize and split into path components
|
||||
let comps = pattern.split(separator: "/", omittingEmptySubsequences: true).map(String.init)
|
||||
|
||||
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: "*?[")
|
||||
|
||||
func isDir(_ path: String) -> Bool {
|
||||
var isDirectory: ObjCBool = false
|
||||
if fm.fileExists(atPath: path, isDirectory: &isDirectory) {
|
||||
return isDirectory.boolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func listDir(_ path: String) -> [String] {
|
||||
(try? fm.contentsOfDirectory(atPath: path)) ?? []
|
||||
}
|
||||
|
||||
// fnmatch against a single path segment (no '/')
|
||||
@inline(__always)
|
||||
func matchSegment(_ name: String, pat: String) -> Bool {
|
||||
name.withCString { nPtr in
|
||||
pat.withCString { pPtr in
|
||||
// FNM_PERIOD -> leading '.' must be matched explicitly (shell-like)
|
||||
// FNM_NOESCAPE -> backslashes are treated literally
|
||||
fnmatch(pPtr, nPtr, FNM_PERIOD | FNM_NOESCAPE) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func real(_ path: String) -> String {
|
||||
URL(fileURLWithPath: path).standardizedFileURL.path
|
||||
}
|
||||
|
||||
func walk(_ base: String, _ idx: Int) {
|
||||
if idx == comps.count {
|
||||
// Only return existing paths
|
||||
if FileManager.default.fileExists(atPath: base) {
|
||||
results.append(real(base))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let part = comps[idx]
|
||||
|
||||
if part == "**" {
|
||||
// Option 1: ** matches zero segments
|
||||
walk(base, idx + 1)
|
||||
|
||||
// Option 2: ** matches one or more directory segments
|
||||
// Recurse into subdirs breadth-first
|
||||
let dirPath = base.isEmpty ? "/" : base
|
||||
let key = real(dirPath)
|
||||
if seenDirs.contains(key) { return }
|
||||
seenDirs.insert(key)
|
||||
|
||||
if isDir(dirPath) {
|
||||
let dirPathNS = dirPath as NSString // Cache the NSString conversion
|
||||
for entry in listDir(dirPath) {
|
||||
let child = dirPathNS.appendingPathComponent(entry)
|
||||
if isDir(child) {
|
||||
// Keep idx the same to allow ** to consume multiple levels
|
||||
walk(child, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Non-** component. If it has no glob metachar, fast-path.
|
||||
let hasMeta = part.rangeOfCharacter(from: globMetaChars) != nil
|
||||
if !hasMeta {
|
||||
let next = (base as NSString).appendingPathComponent(part)
|
||||
walk(next, idx + 1)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
for entry in listDir(dirPath) {
|
||||
if matchSegment(entry, pat: part) {
|
||||
let next = dirPathNS.appendingPathComponent(entry)
|
||||
walk(next, idx + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kick off
|
||||
let isAbs = pattern.hasPrefix("/")
|
||||
let cwd = (base ?? URL.currentDirectory()).path
|
||||
walk(isAbs ? "/" : cwd, 0)
|
||||
|
||||
// De-dup and sort for stability
|
||||
return Array(Set(results)).sorted()
|
||||
}
|
||||
|
|
@ -5,32 +5,493 @@
|
|||
// Created by Sami Samhuri on 2024-04-24.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FileOtter
|
||||
import XCTest
|
||||
|
||||
class FileOtterTests: XCTestCase {
|
||||
final class DirTests: XCTestCase {
|
||||
var tempDir: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
tempDir = URL.temporaryDirectory
|
||||
.appendingPathComponent("FileOtterTests-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
// Any test you write for XCTest can be annotated as throws and async.
|
||||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
if FileManager.default.fileExists(atPath: tempDir.path) {
|
||||
try FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Well-known Directories Tests
|
||||
|
||||
func testCachesDirectory() {
|
||||
let caches = Dir.caches
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: caches.path))
|
||||
XCTAssertTrue(caches.path.contains("Caches"))
|
||||
}
|
||||
|
||||
func testPwd() {
|
||||
let pwd = Dir.pwd
|
||||
|
||||
XCTAssertEqual(pwd, URL.currentDirectory())
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: pwd.path))
|
||||
}
|
||||
|
||||
func testDocumentsDirectory() {
|
||||
let documents = Dir.documents
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: documents.path))
|
||||
XCTAssertTrue(documents.path.contains("Documents"))
|
||||
}
|
||||
|
||||
func testHomeDirectory() {
|
||||
let home = Dir.home
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: home.path))
|
||||
XCTAssertEqual(home.path, URL.homeDirectory.path)
|
||||
}
|
||||
|
||||
func testLibraryDirectory() {
|
||||
let library = Dir.library
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: library.path))
|
||||
XCTAssertTrue(library.path.contains("Library"))
|
||||
}
|
||||
|
||||
// MARK: - chdir Tests
|
||||
|
||||
func testChdirChangesDirectory() throws {
|
||||
let originalDir = Dir.pwd
|
||||
|
||||
XCTAssertNoThrow(try Dir.chdir(tempDir))
|
||||
XCTAssertEqual(Dir.pwd.resolvingSymlinksInPath().path, tempDir.resolvingSymlinksInPath().path)
|
||||
|
||||
try Dir.chdir(originalDir)
|
||||
}
|
||||
|
||||
func testChdirWithBlock() {
|
||||
let originalDir = Dir.pwd
|
||||
let testFile = tempDir.appendingPathComponent("test.txt")
|
||||
|
||||
let result = Dir.chdir(tempDir) { url in
|
||||
XCTAssertEqual(Dir.pwd.resolvingSymlinksInPath().path, tempDir.resolvingSymlinksInPath().path)
|
||||
XCTAssertEqual(url.resolvingSymlinksInPath().path, tempDir.resolvingSymlinksInPath().path)
|
||||
|
||||
try? "Testing chdir block".write(to: testFile, atomically: true, encoding: .utf8)
|
||||
return "success"
|
||||
}
|
||||
|
||||
XCTAssertEqual(result, "success")
|
||||
XCTAssertEqual(Dir.pwd.resolvingSymlinksInPath().path, originalDir.resolvingSymlinksInPath().path)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: testFile.path))
|
||||
}
|
||||
|
||||
func testChdirWithBlockRestoresDirectoryOnError() {
|
||||
let originalDir = Dir.pwd
|
||||
|
||||
do {
|
||||
try Dir.chdir(tempDir) { url in
|
||||
// Verify we're in the temp directory
|
||||
let currentPath = Dir.pwd.resolvingSymlinksInPath().path
|
||||
let expectedPath = tempDir.resolvingSymlinksInPath().path
|
||||
XCTAssertEqual(currentPath, expectedPath)
|
||||
|
||||
// Also verify the passed URL matches
|
||||
XCTAssertEqual(url.resolvingSymlinksInPath().path, expectedPath)
|
||||
|
||||
throw NSError(domain: "TestError", code: 1)
|
||||
}
|
||||
XCTFail("Should have thrown an error")
|
||||
} catch {
|
||||
// Verify we're back in the original directory
|
||||
XCTAssertEqual(Dir.pwd.resolvingSymlinksInPath().path, originalDir.resolvingSymlinksInPath().path)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - unlink/rmdir Tests
|
||||
|
||||
func testUnlinkRemovesDirectory() throws {
|
||||
let dirToRemove = tempDir.appendingPathComponent("dir-to-remove")
|
||||
try FileManager.default.createDirectory(at: dirToRemove, withIntermediateDirectories: true)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: dirToRemove.path))
|
||||
|
||||
XCTAssertNoThrow(try Dir.unlink(dirToRemove))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: dirToRemove.path))
|
||||
}
|
||||
|
||||
func testRmdirRemovesDirectory() throws {
|
||||
let dirToRemove = tempDir.appendingPathComponent("dir-to-rmdir")
|
||||
try FileManager.default.createDirectory(at: dirToRemove, withIntermediateDirectories: true)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: dirToRemove.path))
|
||||
|
||||
XCTAssertNoThrow(try Dir.rmdir(dirToRemove))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: dirToRemove.path))
|
||||
}
|
||||
|
||||
func testUnlinkThrowsForNonExistentDirectory() {
|
||||
let nonExistent = tempDir.appendingPathComponent("does-not-exist")
|
||||
XCTAssertThrowsError(try Dir.unlink(nonExistent))
|
||||
}
|
||||
|
||||
// MARK: - Reading Contents Tests
|
||||
|
||||
func testChildren() throws {
|
||||
let file1 = tempDir.appendingPathComponent("file1.txt")
|
||||
let file2 = tempDir.appendingPathComponent("file2.txt")
|
||||
let subdir = tempDir.appendingPathComponent("subdir")
|
||||
|
||||
try "Content 1".write(to: file1, atomically: true, encoding: .utf8)
|
||||
try "Content 2".write(to: file2, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.createDirectory(at: subdir, withIntermediateDirectories: true)
|
||||
|
||||
let children = try Dir.children(tempDir)
|
||||
XCTAssertEqual(children.count, 3)
|
||||
|
||||
let childNames = children.map { $0.lastPathComponent }.sorted()
|
||||
XCTAssertEqual(childNames, ["file1.txt", "file2.txt", "subdir"])
|
||||
}
|
||||
|
||||
func testExists() throws {
|
||||
XCTAssertTrue(try Dir.exists(tempDir))
|
||||
XCTAssertTrue(try Dir.exists(Dir.home))
|
||||
|
||||
let nonExistent = tempDir.appendingPathComponent("not-a-directory")
|
||||
XCTAssertFalse(try Dir.exists(nonExistent))
|
||||
|
||||
let fileNotDir = tempDir.appendingPathComponent("file.txt")
|
||||
try "I'm a file".write(to: fileNotDir, atomically: true, encoding: .utf8)
|
||||
XCTAssertFalse(try Dir.exists(fileNotDir))
|
||||
}
|
||||
|
||||
func testIsEmpty() throws {
|
||||
XCTAssertTrue(try Dir.isEmpty(tempDir))
|
||||
|
||||
let file = tempDir.appendingPathComponent("file.txt")
|
||||
try "Content".write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
XCTAssertFalse(try Dir.isEmpty(tempDir))
|
||||
}
|
||||
|
||||
// MARK: - Dir Struct and Collection Tests
|
||||
|
||||
func testDirInitialization() throws {
|
||||
let file1 = tempDir.appendingPathComponent("a.txt")
|
||||
let file2 = tempDir.appendingPathComponent("b.txt")
|
||||
try "A".write(to: file1, atomically: true, encoding: .utf8)
|
||||
try "B".write(to: file2, atomically: true, encoding: .utf8)
|
||||
|
||||
let dir = try Dir(url: tempDir)
|
||||
XCTAssertEqual(dir.url, tempDir)
|
||||
XCTAssertEqual(dir.count, 2)
|
||||
}
|
||||
|
||||
func testDirSubscript() throws {
|
||||
let file1 = tempDir.appendingPathComponent("1.txt")
|
||||
let file2 = tempDir.appendingPathComponent("2.txt")
|
||||
let file3 = tempDir.appendingPathComponent("3.txt")
|
||||
try "One".write(to: file1, atomically: true, encoding: .utf8)
|
||||
try "Two".write(to: file2, atomically: true, encoding: .utf8)
|
||||
try "Three".write(to: file3, atomically: true, encoding: .utf8)
|
||||
|
||||
let dir = try Dir(url: tempDir)
|
||||
XCTAssertEqual(dir.count, 3)
|
||||
|
||||
let firstItem = dir[0]
|
||||
XCTAssertTrue(["1.txt", "2.txt", "3.txt"].contains(firstItem.lastPathComponent))
|
||||
}
|
||||
|
||||
func testDirIteration() throws {
|
||||
let files = ["rock.mp3", "jazz.mp3", "punk.mp3"]
|
||||
for filename in files {
|
||||
let file = tempDir.appendingPathComponent(filename)
|
||||
try filename.write(to: file, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
let dir = try Dir(url: tempDir)
|
||||
var foundFiles: [String] = []
|
||||
|
||||
for url in dir {
|
||||
foundFiles.append(url.lastPathComponent)
|
||||
}
|
||||
|
||||
XCTAssertEqual(foundFiles.sorted(), files.sorted())
|
||||
}
|
||||
|
||||
func testDirEquality() throws {
|
||||
let dir1 = try Dir(url: tempDir)
|
||||
let dir2 = try Dir(url: tempDir)
|
||||
XCTAssertEqual(dir1, dir2)
|
||||
|
||||
let otherDir = URL.temporaryDirectory
|
||||
.appendingPathComponent("other-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: otherDir, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: otherDir) }
|
||||
|
||||
let dir3 = try Dir(url: otherDir)
|
||||
XCTAssertNotEqual(dir1, dir3)
|
||||
}
|
||||
|
||||
// MARK: - Glob Tests
|
||||
|
||||
func testGlobBasicWildcard() throws {
|
||||
// Create test files
|
||||
try "content1".write(to: tempDir.appendingPathComponent("file1.txt"), atomically: true, encoding: .utf8)
|
||||
try "content2".write(to: tempDir.appendingPathComponent("file2.txt"), atomically: true, encoding: .utf8)
|
||||
try "content3".write(to: tempDir.appendingPathComponent("file3.md"), atomically: true, encoding: .utf8)
|
||||
|
||||
let results = Dir.glob(base: tempDir, "*.txt")
|
||||
XCTAssertEqual(results.count, 2)
|
||||
|
||||
let filenames = results.map { $0.lastPathComponent }.sorted()
|
||||
XCTAssertEqual(filenames, ["file1.txt", "file2.txt"])
|
||||
}
|
||||
|
||||
func testGlobSubscript() throws {
|
||||
// Create test files
|
||||
try "swift1".write(to: tempDir.appendingPathComponent("app.swift"), atomically: true, encoding: .utf8)
|
||||
try "swift2".write(to: tempDir.appendingPathComponent("lib.swift"), atomically: true, encoding: .utf8)
|
||||
try "objc".write(to: tempDir.appendingPathComponent("bridge.m"), atomically: true, encoding: .utf8)
|
||||
|
||||
let swiftFiles = Dir[tempDir, "*.swift"]
|
||||
XCTAssertEqual(swiftFiles.count, 2)
|
||||
|
||||
let allFiles = Dir[tempDir, "*.*"]
|
||||
XCTAssertEqual(allFiles.count, 3)
|
||||
}
|
||||
|
||||
func testGlobQuestionMark() throws {
|
||||
// Create test files with pattern
|
||||
try "a".write(to: tempDir.appendingPathComponent("a1.txt"), atomically: true, encoding: .utf8)
|
||||
try "b".write(to: tempDir.appendingPathComponent("b2.txt"), atomically: true, encoding: .utf8)
|
||||
try "c".write(to: tempDir.appendingPathComponent("abc.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let results = Dir.glob(base: tempDir, "??.txt")
|
||||
XCTAssertEqual(results.count, 2)
|
||||
|
||||
let filenames = results.map { $0.lastPathComponent }.sorted()
|
||||
XCTAssertEqual(filenames, ["a1.txt", "b2.txt"])
|
||||
}
|
||||
|
||||
func testGlobCharacterClass() throws {
|
||||
// Create test files
|
||||
try "1".write(to: tempDir.appendingPathComponent("test1.txt"), atomically: true, encoding: .utf8)
|
||||
try "2".write(to: tempDir.appendingPathComponent("test2.txt"), atomically: true, encoding: .utf8)
|
||||
try "a".write(to: tempDir.appendingPathComponent("testa.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let numberFiles = Dir.glob(base: tempDir, "test[0-9].txt")
|
||||
XCTAssertEqual(numberFiles.count, 2)
|
||||
|
||||
let letterFiles = Dir.glob(base: tempDir, "test[a-z].txt")
|
||||
XCTAssertEqual(letterFiles.count, 1)
|
||||
XCTAssertEqual(letterFiles.first?.lastPathComponent, "testa.txt")
|
||||
}
|
||||
|
||||
func testGlobRecursive() throws {
|
||||
// Create nested directory structure
|
||||
let subdir1 = tempDir.appendingPathComponent("src")
|
||||
let subdir2 = subdir1.appendingPathComponent("lib")
|
||||
try FileManager.default.createDirectory(at: subdir2, withIntermediateDirectories: true)
|
||||
|
||||
try "root".write(to: tempDir.appendingPathComponent("root.swift"), atomically: true, encoding: .utf8)
|
||||
try "src".write(to: subdir1.appendingPathComponent("main.swift"), atomically: true, encoding: .utf8)
|
||||
try "lib".write(to: subdir2.appendingPathComponent("utils.swift"), atomically: true, encoding: .utf8)
|
||||
try "lib2".write(to: subdir2.appendingPathComponent("helpers.swift"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Test ** for recursive matching
|
||||
let allSwiftFiles = Dir.glob(base: tempDir, "**/*.swift")
|
||||
XCTAssertEqual(allSwiftFiles.count, 4)
|
||||
|
||||
// Test ** matching zero segments
|
||||
let srcSwiftFiles = Dir.glob(base: tempDir, "src/**/*.swift")
|
||||
XCTAssertEqual(srcSwiftFiles.count, 3) // main.swift, utils.swift, helpers.swift
|
||||
|
||||
// Test specific path with **
|
||||
let libSwiftFiles = Dir.glob(base: tempDir, "**/lib/*.swift")
|
||||
XCTAssertEqual(libSwiftFiles.count, 2) // utils.swift, helpers.swift
|
||||
}
|
||||
|
||||
func testGlobNoMatches() {
|
||||
let results = Dir.glob(base: tempDir, "*.nonexistent")
|
||||
XCTAssertTrue(results.isEmpty)
|
||||
|
||||
let subscriptResults = Dir[tempDir, "no-such-file.*"]
|
||||
XCTAssertTrue(subscriptResults.isEmpty)
|
||||
}
|
||||
|
||||
func testGlobHiddenFiles() throws {
|
||||
// Create hidden file
|
||||
try "hidden".write(to: tempDir.appendingPathComponent(".hidden.txt"), atomically: true, encoding: .utf8)
|
||||
try "visible".write(to: tempDir.appendingPathComponent("visible.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
// By default, * should not match hidden files (FNM_PERIOD flag)
|
||||
let starResults = Dir.glob(base: tempDir, "*.txt")
|
||||
XCTAssertEqual(starResults.count, 1)
|
||||
XCTAssertEqual(starResults.first?.lastPathComponent, "visible.txt")
|
||||
|
||||
// Explicitly matching hidden files
|
||||
let hiddenResults = Dir.glob(base: tempDir, ".*.txt")
|
||||
XCTAssertEqual(hiddenResults.count, 1)
|
||||
XCTAssertEqual(hiddenResults.first?.lastPathComponent, ".hidden.txt")
|
||||
}
|
||||
|
||||
// MARK: - Debug Description Tests
|
||||
|
||||
func testDebugDescription() throws {
|
||||
let dir = try Dir(url: tempDir)
|
||||
let debugDescription = String(reflecting: dir)
|
||||
XCTAssertEqual(debugDescription, "<Dir:\(tempDir.path)>")
|
||||
|
||||
let homeDir = try Dir(url: Dir.home)
|
||||
let homeDebugDescription = String(reflecting: homeDir)
|
||||
XCTAssertEqual(homeDebugDescription, "<Dir:\(Dir.home.path)>")
|
||||
}
|
||||
|
||||
func testDescription() throws {
|
||||
let dir = try Dir(url: tempDir)
|
||||
let description = String(describing: dir)
|
||||
XCTAssertEqual(description, tempDir.path)
|
||||
|
||||
let homeDir = try Dir(url: Dir.home)
|
||||
let homeDescription = String(describing: homeDir)
|
||||
XCTAssertEqual(homeDescription, Dir.home.path)
|
||||
}
|
||||
|
||||
// MARK: - tmpdir Tests
|
||||
|
||||
func testTmp() {
|
||||
let tmp = Dir.tmp
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmp.path))
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
FileManager.default.fileExists(atPath: tmp.path, isDirectory: &isDirectory)
|
||||
XCTAssertTrue(isDirectory.boolValue)
|
||||
|
||||
XCTAssertEqual(tmp, URL.temporaryDirectory)
|
||||
}
|
||||
|
||||
// MARK: - mkdir Tests
|
||||
|
||||
func testMkdir() throws {
|
||||
let newDir = tempDir.appendingPathComponent("test-mkdir")
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: newDir.path))
|
||||
|
||||
try Dir.mkdir(newDir)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: newDir.path))
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
FileManager.default.fileExists(atPath: newDir.path, isDirectory: &isDirectory)
|
||||
XCTAssertTrue(isDirectory.boolValue)
|
||||
}
|
||||
|
||||
func testMkdirWithPermissions() throws {
|
||||
let newDir = tempDir.appendingPathComponent("test-mkdir-perms")
|
||||
|
||||
try Dir.mkdir(newDir, permissions: 0o700)
|
||||
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: newDir.path)
|
||||
let permissions = attributes[.posixPermissions] as? Int
|
||||
XCTAssertNotNil(permissions)
|
||||
|
||||
#if os(macOS) || os(Linux)
|
||||
XCTAssertEqual(permissions! & 0o777, 0o700)
|
||||
#endif
|
||||
}
|
||||
|
||||
func testMkdirFailsIfDirectoryExists() throws {
|
||||
let existingDir = tempDir.appendingPathComponent("existing")
|
||||
try Dir.mkdir(existingDir)
|
||||
|
||||
XCTAssertThrowsError(try Dir.mkdir(existingDir)) { error in
|
||||
XCTAssertTrue(error is CocoaError)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - mktmpdir Tests
|
||||
|
||||
func testMktmpdir() throws {
|
||||
let tmpDir = try Dir.mktmpdir()
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir.path))
|
||||
XCTAssertTrue(tmpDir.path.contains("d-"))
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
FileManager.default.fileExists(atPath: tmpDir.path, isDirectory: &isDirectory)
|
||||
XCTAssertTrue(isDirectory.boolValue)
|
||||
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: tmpDir.path)
|
||||
let permissions = attributes[.posixPermissions] as? Int
|
||||
XCTAssertNotNil(permissions)
|
||||
|
||||
#if os(macOS) || os(Linux)
|
||||
XCTAssertEqual(permissions! & 0o777, 0o700)
|
||||
#endif
|
||||
|
||||
try FileManager.default.removeItem(at: tmpDir)
|
||||
}
|
||||
|
||||
func testMktempdirWithPrefix() throws {
|
||||
let tmpDir = try Dir.mktmpdir(prefix: "fileotter")
|
||||
XCTAssertTrue(tmpDir.path.contains("fileotter-"))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir.path))
|
||||
|
||||
try FileManager.default.removeItem(at: tmpDir)
|
||||
}
|
||||
|
||||
func testMktempdirWithPrefixAndSuffix() throws {
|
||||
let tmpDir = try Dir.mktmpdir(prefix: "test", suffix: "tmp")
|
||||
XCTAssertTrue(tmpDir.lastPathComponent.hasPrefix("test-"))
|
||||
XCTAssertTrue(tmpDir.lastPathComponent.hasSuffix("-tmp"))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir.path))
|
||||
|
||||
try FileManager.default.removeItem(at: tmpDir)
|
||||
}
|
||||
|
||||
func testMktempdirWithBlock() throws {
|
||||
var blockExecuted = false
|
||||
var tmpDirInBlock: URL?
|
||||
var fileCreated = false
|
||||
|
||||
let result = try Dir.mktmpdir(prefix: "block") { tmpDir in
|
||||
blockExecuted = true
|
||||
tmpDirInBlock = tmpDir
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir.path))
|
||||
|
||||
let testFile = tmpDir.appendingPathComponent("test.txt")
|
||||
try "Hello from mktmpdir block".write(to: testFile, atomically: true, encoding: .utf8)
|
||||
fileCreated = FileManager.default.fileExists(atPath: testFile.path)
|
||||
|
||||
return "block result"
|
||||
}
|
||||
|
||||
XCTAssertTrue(blockExecuted)
|
||||
XCTAssertEqual(result, "block result")
|
||||
XCTAssertTrue(fileCreated)
|
||||
|
||||
if let tmpDirInBlock = tmpDirInBlock {
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: tmpDirInBlock.path))
|
||||
}
|
||||
}
|
||||
|
||||
func testMktempdirWithBlockThrowingError() {
|
||||
do {
|
||||
try Dir.mktmpdir { tmpDir in
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir.path))
|
||||
throw NSError(domain: "TestError", code: 42)
|
||||
}
|
||||
XCTFail("Should have thrown an error")
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
XCTAssertEqual(nsError.domain, "TestError")
|
||||
XCTAssertEqual(nsError.code, 42)
|
||||
}
|
||||
}
|
||||
|
||||
func testMktempdirEachCallCreatesUniqueDirectory() throws {
|
||||
let tmpDir1 = try Dir.mktmpdir()
|
||||
let tmpDir2 = try Dir.mktmpdir()
|
||||
|
||||
XCTAssertNotEqual(tmpDir1, tmpDir2)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir1.path))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tmpDir2.path))
|
||||
|
||||
try FileManager.default.removeItem(at: tmpDir1)
|
||||
try FileManager.default.removeItem(at: tmpDir2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue