Merge pull request #26 from mxcl/codecov

Increase code coverage
This commit is contained in:
Max Howell
2019-01-31 12:25:41 -05:00
committed by GitHub
7 changed files with 182 additions and 19 deletions

View File

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

View File

@@ -130,6 +130,9 @@ public extension Path {
Deletes the path, recursively if a directory. Deletes the path, recursively if a directory.
- Note: noop: if the path doesnt exist - Note: noop: if the path doesnt exist
*Path.swift* doesnt error if desired end result preexists. *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 @inlinable
func delete() throws { func delete() throws {

View File

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

View File

@@ -1,4 +1,9 @@
import Foundation import Foundation
#if os(Linux)
import func Glibc.access
#else
import func Darwin.access
#endif
public extension Path { public extension Path {
//MARK: Filesystem Properties //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. /// Returns true if the path represents an actual file that is also deletable by the current user.
var isDeletable: Bool { 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. /// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool { 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

@@ -204,4 +204,121 @@ class PathTests: XCTestCase {
} }
} }
} }
func testCommonDirectories() {
XCTAssertEqual(Path.root.string, "/")
XCTAssertEqual(Path.home.string, NSHomeDirectory())
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
#if os(macOS)
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
}
}
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)
// doesnt exist in tests
_ = Bundle.main.sharedFrameworks
}
func testFileHandleExtensions() throws {
_ = try FileHandle(forReadingAt: Path(#file)!)
_ = try FileHandle(forWritingAt: Path(#file)!)
}
} }

View File

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

View File

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