Compare commits

...

17 Commits

Author SHA1 Message Date
Max Howell
6b52932e7b Merge pull request #27 from mxcl/codecov
More coverage
2019-01-31 16:29:08 -05:00
Max Howell
c456081e65 More coverage 2019-01-31 14:26:09 -05:00
Max Howell
ec6c0113f9 Remaining code coverage
Can’t do coverage for Linux unfortunately.
2019-01-31 13:11:50 -05:00
Max Howell
7970c4d8a7 Document robustness & XP
[ci skip]
2019-01-31 12:38:41 -05:00
Max Howell
e342da8644 Remove auto-tagging since I can do this manually 2019-01-31 12:28:16 -05:00
Max Howell
9aa91c649e Merge pull request #26 from mxcl/codecov
Increase code coverage
2019-01-31 12:25:41 -05:00
Max Howell
62ea0d47b3 Fixes & increased code coverage 2019-01-31 12:15:31 -05:00
repo-ranger[bot]
ab9a70e947 Merge pull request #25 from mxcl/rename
Refactor rename -> rename(to:)
2019-01-31 15:32:59 +00:00
Max Howell
49ef073e34 Refactor rename -> rename(to:) 2019-01-31 10:15:39 -05:00
Max Howell
889d825b3a Merge pull request #24 from mxcl/rename
Rename
2019-01-31 10:08:02 -05:00
Max Howell
f1cd06fdff Add CI post success hook yamls 2019-01-31 09:58:46 -05:00
Max Howell
c6e840b9b6 Add rename 2019-01-31 08:39:54 -05:00
Max Howell
eb34ac4af8 Add overwrite parameter to move(into:) 2019-01-31 08:37:32 -05:00
Max Howell
66ae86c986 Enable codecov.io 2019-01-31 08:37:14 -05:00
Max Howell
c432f710eb Merge pull request #22 from mxcl/files()
Entry.files defaults to all files
2019-01-28 12:17:08 -05:00
Max Howell
19c0c19bb6 Entry.files defaults to all files 2019-01-28 12:04:47 -05:00
Max Howell
ee1f46954c Fixes #20
[skip ci]
2019-01-28 11:05:51 -05:00
13 changed files with 427 additions and 58 deletions

2
.github/codecov.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
ignore:
- Tests

2
.github/ranger.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
merges:
- action: delete_branch

View File

@@ -19,9 +19,10 @@ jobs:
name: macOS / Swift 4.2.1
- &xcodebuild
before_install: swift package generate-xcodeproj
before_install: swift package generate-xcodeproj --enable-code-coverage
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS / Swift 4.2.1
after_success: bash <(curl -s https://codecov.io/bash)
- <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS / Swift 4.2.1
@@ -34,6 +35,7 @@ jobs:
-scheme Path.swift-Package \
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty
after_success: false
- &linux
env: SWIFT_VERSION=4.2.1

View File

@@ -1,4 +1,4 @@
# Path.swift ![badge-platforms] ![badge-languages][] [![Build Status](https://travis-ci.com/mxcl/Path.swift.svg)](https://travis-ci.com/mxcl/Path.swift)
# Path.swift ![badge-platforms][] ![badge-languages][] [![badge-ci][]][travis] [![badge-jazzy][]][docs] [![badge-codecov][]][codecov] ![badge-version][]
A file-system pathing library focused on developer experience and robust end
results.
@@ -33,7 +33,7 @@ print(foo) // => /bar/foo
print(foo.isFile) // => true
// we support dynamic members (_use_sparingly_):
let prefs = Path.home.Library.Preferences
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
// a practical example: installing a helper executable
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
@@ -57,7 +57,7 @@ help me continue my work, I appreciate it x
# Handbook
Our [online API documentation] is automatically updated for new releases.
Our [online API documentation][docs] is automatically updated for new releases.
## Codable
@@ -107,8 +107,9 @@ let ls = Path.root.usr.bin.ls // => /usr/bin/ls
```
This is less commonly useful than you would think, hence our documentation
does not use it. Usually you are joining variables or other `String` arguments.
However when you need it, its *lovely*.
does not use it. Usually you are joining variables or other `String` arguments
or trying to describe files (and files usually have extensions). However when
you need it, its *lovely*.
## Initializing from user-input
@@ -160,13 +161,27 @@ for entry in Path.home.ls() where entry.path.mtime > yesterday {
//
}
let dirs = Path.home.ls().directories().filter {
//
}
let dirs = Path.home.ls().directories
let files = Path.home.ls().files
let swiftFiles = Path.home.ls().files(withExtension: "swift")
```
# `Path.swift` is robust
Some parts of `FileManager` are not exactly idiomatic. For example
`isExecutableFile` returns `true` even if there is no file there, it is instead
telling you that *if* you made a file there it *could* be executable. Thus we
check the POSIX permissions of the file first, before returning the result of
`isExecutableFile`. `Path.swift` has done the leg-work for you so you can get on
with your work without worries.
# `Path.swift` is properly cross-platform
`FileManager` on Linux is full of holes. We have found the holes and worked
round them where necessary.
# Rules & Caveats
Paths are just string representations, there *might not* be a real file there.
@@ -199,7 +214,7 @@ Path("~/foo")! // => /Users/mxcl/foo
Path("~foo") // => nil
```
*Path.swift* has the general policty that if the desired end result preexists,
*Path.swift* has the general policy that if the desired end result preexists,
then its a noop:
* If you try to delete a file, but the file doesn't exist, we do nothing.
@@ -220,7 +235,7 @@ package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.5.0"
CocoaPods:
```ruby
pod 'Path.swift' ~> '0.5.0'
pod 'Path.swift', '~> 0.5'
```
Carthage:
@@ -245,4 +260,10 @@ https://codebasesaga.com/canopy/
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg
[online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html
[docs]: https://mxcl.github.io/Path.swift/Structs/Path.html
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true
[badge-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg
[badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg
[travis]: https://travis-ci.com/mxcl/Path.swift
[codecov]: https://codecov.io/gh/mxcl/Path.swift
[badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version

View File

@@ -1,34 +1,35 @@
import Foundation
//#if os(Linux)
//import func Glibc.chmod
//#endif
public extension Path {
//MARK: Filesystem Attributes
/**
Returns the creation-time of the file.
- Note: Returns UNIX-time-zero if there is no creation-time, this should only happen if the file doesnt exist.
- Note: Returns `nil` if there is no creation-time, this should only happen if the file doesnt exist.
- Important: On Linux this is filesystem dependendent and may not exist.
*/
var ctime: Date {
var ctime: Date? {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0)
return attrs[.creationDate] as? Date
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
return nil
}
}
/**
Returns the modification-time of the file.
- Note: Returns the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, this should only happen if the file doesnt exist.
- Note: If this returns `nil` and the file exists, something is very wrong.
*/
var mtime: Date {
var mtime: Date? {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? ctime
return attrs[.modificationDate] as? Date
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
return nil
}
}
@@ -39,27 +40,40 @@ public extension Path {
*/
@discardableResult
func chmod(_ octal: Int) throws -> Path {
// #if os(Linux)
// Glibc.chmod(string, __mode_t(octal))
// #else
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
// #endif
return self
}
/// - Note: If file is already locked, does nothing
/// - Note: If file doesnt exist, throws
/**
- Note: If file is already locked, does nothing.
- Note: If file doesnt exist, throws.
- Important: On Linux does nothing.
*/
@discardableResult
func lock() throws -> Path {
#if !os(Linux)
var attrs = try FileManager.default.attributesOfItem(atPath: string)
let b = attrs[.immutable] as? Bool ?? false
if !b {
attrs[.immutable] = true
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
return self
}
/// - Note: If file isnt locked, does nothing
/// - Note: If file doesnt exist, does nothing
/**
- Note: If file isnt locked, does nothing.
- Note: If file doesnt exist, does nothing.
- Important: On Linux does nothing.
*/
@discardableResult
func unlock() throws -> Path {
#if !os(Linux)
var attrs: [FileAttributeKey: Any]
do {
attrs = try FileManager.default.attributesOfItem(atPath: string)
@@ -71,6 +85,7 @@ public extension Path {
attrs[.immutable] = false
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
return self
}
}

View File

@@ -1,4 +1,7 @@
import Foundation
#if os(Linux)
import Glibc
#endif
public extension Path {
//MARK: File Management
@@ -25,6 +28,11 @@ public extension Path {
if overwrite, to.isFile, isFile {
try FileManager.default.removeItem(at: to.url)
}
#if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, to.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
try FileManager.default.copyItem(atPath: string, toPath: to.string)
return to
}
@@ -53,20 +61,16 @@ public extension Path {
@discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: into.url, withIntermediateDirectories: true)
}
let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
}
#if os(Linux)
#if swift(>=5.1)
// check if fixed
#else
#if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, rv.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
#endif
try FileManager.default.copyItem(at: url, to: rv.url)
return rv
@@ -90,7 +94,7 @@ public extension Path {
*/
@discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists {
if overwrite, to.isFile {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.moveItem(at: url, to: to.url)
@@ -112,13 +116,16 @@ public extension Path {
- SeeAlso: move(into:overwrite:)
*/
@discardableResult
func move(into: Path) throws -> Path {
func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try into.mkdir(.p)
} else if !into.isDirectory {
throw CocoaError.error(.fileWriteFileExists)
}
let rv = into/basename()
if overwrite, rv.isFile {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
}
@@ -127,6 +134,9 @@ public extension Path {
Deletes the path, recursively if a directory.
- Note: noop: if the path doesnt exist
*Path.swift* doesnt error if desired end result preexists.
- Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission.
- Note: This function will fail if the file or directory is locked
- SeeAlso: `lock()`
*/
@inlinable
func delete() throws {
@@ -136,13 +146,26 @@ public extension Path {
}
/**
Creates an empty file at this path.
Creates an empty file at this path or if the file exists, updates its modification time.
- Returns: `self` to allow chaining.
*/
@inlinable
@discardableResult
func touch() throws -> Path {
return try "".write(to: self)
if !exists {
guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown)
}
} else {
#if os(Linux)
let fd = open(string, O_WRONLY)
defer { close(fd) }
futimens(fd, nil)
#else
try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string)
#endif
}
return self
}
/**
@@ -171,6 +194,21 @@ public extension Path {
}
return self
}
/**
Renames the file at path.
Path.root.foo.bar.rename(to: "baz") // => /foo/baz
- Parameter to: the new basename for the file
- Returns: The renamed path.
*/
@discardableResult
func rename(to newname: String) throws -> Path {
let newpath = parent/newname
try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
return newpath
}
}
/// Options for `Path.mkdir(_:)`

View File

@@ -10,6 +10,6 @@ extension Path: CustomStringConvertible {
extension Path: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")`
public var debugDescription: String {
return "Path(string: \(string))"
return "Path(\(string))"
}
}

View File

@@ -51,7 +51,14 @@ public extension Array where Element == Entry {
}
}
/// Filters the list of entries to be a list of Paths that are files with the specified extension
/// Filters the list of entries to be a list of Paths that are files.
var files: [Path] {
return compactMap {
$0.kind == .file ? $0.path : nil
}
}
/// Filters the list of entries to be a list of Paths that are files with the specified extension.
func files(withExtension ext: String) -> [Path] {
return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,4 +1,9 @@
import Foundation
#if os(Linux)
import func Glibc.access
#else
import func Darwin.access
#endif
public extension Path {
//MARK: Filesystem Properties
@@ -32,11 +37,22 @@ public extension Path {
/// Returns true if the path represents an actual file that is also deletable by the current user.
var isDeletable: Bool {
return FileManager.default.isDeletableFile(atPath: string)
#if os(Linux) && !swift(>=5.1)
return exists && access(parent.string, W_OK) == 0
#else
// FileManager.isDeletableFile returns true if there is *not* a file there
return exists && FileManager.default.isDeletableFile(atPath: string)
#endif
}
/// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string)
if access(string, X_OK) == 0 {
// FileManager.isExxecutableFile returns true even if there is *not*
// a file there *but* if there was it could be *made* executable
return FileManager.default.isExecutableFile(atPath: string)
} else {
return false
}
}
}

View File

@@ -9,7 +9,7 @@ import Foundation
`Path` supports `Codable`, and can be configured to
[encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable).
Sorting a `Sequence` of `Path`s will return the locale-aware sort order, which
Sorting a `Sequence` of paths will return the locale-aware sort order, which
will give you the same order as Finder.
Converting from a `String` is a common first step, here are the recommended
@@ -20,13 +20,12 @@ import Foundation
let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput
If you are constructing Paths from static-strings we provide support for
If you are constructing paths from static-strings we provide support for
dynamic members:
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
- Note: There may not be an actual filesystem entry at the path. The underlying
representation for `Path` is `String`.
- Note: A `Path` does not necessarily represent an actual filesystem entry.
*/
@dynamicMemberLookup
@@ -36,7 +35,7 @@ public struct Path: Equatable, Hashable, Comparable {
self.string = string
}
/// Returns `nil` unless fed an absolute path
/// Returns `nil` unless fed an absolute path.
public init?(_ description: String) {
guard description.starts(with: "/") || description.starts(with: "~/") else { return nil }
self.init(string: (description as NSString).standardizingPath)
@@ -73,10 +72,15 @@ public struct Path: Equatable, Hashable, Comparable {
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
- Note: We special case eg. `foo.tar.gz`.
*/
@inlinable
public var `extension`: String {
return (string as NSString).pathExtension
if string.hasSuffix(".tar.gz") {
return "tar.gz"
} else {
return (string as NSString).pathExtension
}
}
//MARK: Pathing
@@ -91,7 +95,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter pathComponent: The string to join with this path.
- Returns: A new joined path.
- SeeAlso: `Path./(_:, _:)`
- SeeAlso: `Path./(_:_:)`
*/
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion)

View File

@@ -15,20 +15,26 @@ class PathTests: XCTestCase {
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
try tmpdir.a.mkdir().c.touch()
try tmpdir.b.touch()
try tmpdir.join("b.swift").touch()
try tmpdir.c.touch()
try tmpdir.join(".d").mkdir().e.touch()
var paths = Set<String>()
let lsrv = try tmpdir.ls()
var dirs = 0
for entry in try tmpdir.ls() {
for entry in lsrv {
if entry.kind == .directory {
dirs += 1
}
paths.insert(entry.path.basename())
}
XCTAssertEqual(dirs, 2)
XCTAssertEqual(paths, ["a", "b", "c", ".d"])
XCTAssertEqual(dirs, lsrv.directories.count)
XCTAssertEqual(["a", ".d"], Set(lsrv.directories.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["b.swift", "c"], Set(lsrv.files.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["b.swift"], Set(lsrv.files(withExtension: "swift").map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["c"], Set(lsrv.files(withExtension: "").map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"])
}
@@ -69,6 +75,18 @@ class PathTests: XCTestCase {
XCTAssert((Path.root/"bin").isDirectory)
}
func testExtension() {
XCTAssertEqual(Path.root.join("a.swift").extension, "swift")
XCTAssertEqual(Path.root.join("a").extension, "")
XCTAssertEqual(Path.root.join("a.").extension, "")
XCTAssertEqual(Path.root.join("a..").extension, "")
XCTAssertEqual(Path.root.join("a..swift").extension, "swift")
XCTAssertEqual(Path.root.join("a..swift.").extension, "")
XCTAssertEqual(Path.root.join("a.tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar..gz").extension, "gz")
}
func testMktemp() throws {
var path: Path!
try Path.mktemp {
@@ -146,14 +164,237 @@ class PathTests: XCTestCase {
XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
}
func testCopyInto() throws {
func testCopyTo() throws {
try Path.mktemp { root in
let bar = try root.join("bar").touch()
try Path.mktemp { root in
try root.join("bar").touch()
XCTAssertThrowsError(try bar.copy(into: root))
try bar.copy(into: root, overwrite: true)
try root.foo.touch().copy(to: root.bar)
XCTAssert(root.foo.isFile)
XCTAssert(root.bar.isFile)
XCTAssertThrowsError(try root.foo.copy(to: root.bar))
try root.foo.copy(to: root.bar, overwrite: true)
}
}
func testCopyInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.copy(into: root2))
try bar1.copy(into: root2, overwrite: true)
XCTAssertTrue(bar1.exists)
XCTAssertTrue(bar2.exists)
}
// test creates intermediary directories
try bar1.copy(into: root1.create.directories)
// test doesnt replace file if copy into a file
let d = try root1.fuz.touch()
XCTAssertThrowsError(try root1.baz.touch().copy(into: d))
XCTAssert(d.isFile)
XCTAssert(root1.baz.isFile)
}
}
func testMoveTo() throws {
try Path.mktemp { tmpdir in
try tmpdir.foo.touch().move(to: tmpdir.bar)
XCTAssertFalse(tmpdir.foo.exists)
XCTAssert(tmpdir.bar.isFile)
XCTAssertThrowsError(try tmpdir.foo.touch().move(to: tmpdir.bar))
try tmpdir.foo.move(to: tmpdir.bar, overwrite: true)
}
}
func testMoveInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.move(into: root2))
try bar1.move(into: root2, overwrite: true)
XCTAssertFalse(bar1.exists)
XCTAssertTrue(bar2.exists)
}
// test creates intermediary directories
try root1.baz.touch().move(into: root1.create.directories)
XCTAssertFalse(root1.baz.exists)
XCTAssert(root1.create.directories.baz.isFile)
// test doesnt replace file if move into a file
let d = try root1.fuz.touch()
XCTAssertThrowsError(try root1.baz.touch().move(into: d))
XCTAssert(d.isFile)
XCTAssert(root1.baz.isFile)
}
}
func testRename() throws {
try Path.mktemp { root in
do {
let file = try root.bar.touch()
let foo = try file.rename(to: "foo")
XCTAssertFalse(file.exists)
XCTAssertTrue(foo.isFile)
}
do {
let file = try root.bar.touch()
XCTAssertThrowsError(try file.rename(to: "foo"))
}
}
}
func testCommonDirectories() {
XCTAssertEqual(Path.root.string, "/")
XCTAssertEqual(Path.home.string, NSHomeDirectory())
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
#if !os(Linux)
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
XCTAssertEqual(Path.applicationSupport.string, NSHomeDirectory() + "/Library/Application Support")
#endif
}
func testStringConvertibles() {
XCTAssertEqual(Path.root.description, "/")
XCTAssertEqual(Path.root.debugDescription, "Path(/)")
}
func testFilesystemAttributes() throws {
XCTAssert(Path(#file)!.isFile)
XCTAssert(Path(#file)!.isReadable)
XCTAssert(Path(#file)!.isWritable)
XCTAssert(Path(#file)!.isDeletable)
XCTAssert(Path(#file)!.parent.isDirectory)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.isDirectory)
XCTAssertFalse(tmpdir.isFile)
let bar = try tmpdir.bar.touch().chmod(0o000)
XCTAssertFalse(bar.isReadable)
XCTAssertFalse(bar.isWritable)
XCTAssertFalse(bar.isDirectory)
XCTAssertFalse(bar.isExecutable)
XCTAssertTrue(bar.isFile)
XCTAssertTrue(bar.isDeletable) // can delete even if no read permissions
try bar.chmod(0o777)
XCTAssertTrue(bar.isReadable)
XCTAssertTrue(bar.isWritable)
XCTAssertTrue(bar.isDeletable)
XCTAssertTrue(bar.isExecutable)
try bar.delete()
XCTAssertFalse(bar.exists)
XCTAssertFalse(bar.isReadable)
XCTAssertFalse(bar.isWritable)
XCTAssertFalse(bar.isExecutable)
XCTAssertFalse(bar.isDeletable)
let nonExistantFile = tmpdir.baz
XCTAssertFalse(nonExistantFile.exists)
XCTAssertFalse(nonExistantFile.isExecutable)
XCTAssertFalse(nonExistantFile.isReadable)
XCTAssertFalse(nonExistantFile.isWritable)
XCTAssertFalse(nonExistantFile.isDeletable)
XCTAssertFalse(nonExistantFile.isDirectory)
XCTAssertFalse(nonExistantFile.isFile)
let baz = try tmpdir.baz.touch()
XCTAssertTrue(baz.isDeletable)
try tmpdir.chmod(0o500) // remove write permission on directory
XCTAssertFalse(baz.isDeletable) // this is how deletion is prevented on UNIX
}
}
func testTimes() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let now1 = Date().timeIntervalSince1970.rounded(.down)
#if !os(Linux)
XCTAssertEqual(foo.ctime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
#endif
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
sleep(1)
try foo.touch()
let now2 = Date().timeIntervalSince1970.rounded(.down)
XCTAssertNotEqual(now1, now2)
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now2) //FIXME flakey
XCTAssertNil(tmpdir.void.mtime)
XCTAssertNil(tmpdir.void.ctime)
}
}
func testDelete() throws {
try Path.mktemp { tmpdir in
try tmpdir.bar1.delete()
try tmpdir.bar2.touch().delete()
try tmpdir.bar3.touch().chmod(0o000).delete()
#if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif
}
}
func testRelativeCodable() throws {
let path = Path.home.foo
let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home
let data = try encoder.encode([path])
let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home
XCTAssertEqual(try decoder.decode([Path].self, from: data), [path])
decoder.userInfo[.relativePath] = Path.documents
XCTAssertEqual(try decoder.decode([Path].self, from: data), [Path.documents.foo])
XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data))
}
func testBundleExtensions() {
XCTAssertTrue(Bundle.main.path.isDirectory)
XCTAssertTrue(Bundle.main.resources.isDirectory)
// dont exist in tests
_ = Bundle.main.path(forResource: "foo", ofType: "bar")
_ = Bundle.main.sharedFrameworks
}
func testDataExtensions() throws {
let data = try Data(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try data.write(to: tmpdir.foo)
_ = try data.write(to: tmpdir.foo, atomically: true)
}
}
func testStringExtensions() throws {
let string = try String(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try string.write(to: tmpdir.foo)
}
}
func testFileHandleExtensions() throws {
_ = try FileHandle(forReadingAt: Path(#file)!)
_ = try FileHandle(forWritingAt: Path(#file)!)
_ = try FileHandle(forUpdatingAt: Path(#file)!)
}
func testSort() {
XCTAssertEqual([Path.root.a, Path.root.c, Path.root.b].sorted(), [Path.root.a, Path.root.b, Path.root.c])
}
func testLock() throws {
#if !os(Linux)
try Path.mktemp { tmpdir in
let bar = try tmpdir.bar.touch()
try bar.lock()
XCTAssertThrowsError(try bar.touch())
try bar.unlock()
try bar.touch()
}
#endif
}
}

View File

@@ -42,7 +42,11 @@ class TemporaryDirectory {
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
do {
try path.chmod(0o777).delete()
} catch {
//TODO log
}
}
}

View File

@@ -3,19 +3,36 @@ import XCTest
extension PathTests {
static let __allTests = [
("testBasename", testBasename),
("testBundleExtensions", testBundleExtensions),
("testCodable", testCodable),
("testCommonDirectories", testCommonDirectories),
("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testCopyTo", testCopyTo),
("testDataExtensions", testDataExtensions),
("testDelete", testDelete),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists),
("testExtension", testExtension),
("testFileHandleExtensions", testFileHandleExtensions),
("testFilesystemAttributes", testFilesystemAttributes),
("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testLock", testLock),
("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo),
("testRelativeCodable", testRelativeCodable),
("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo),
("testRename", testRename),
("testSort", testSort),
("testStringConvertibles", testStringConvertibles),
("testStringExtensions", testStringExtensions),
("testTimes", testTimes),
]
}