Compare commits

...

26 Commits

Author SHA1 Message Date
Max Howell
39f81ae258 Fix pods deploy 2021-05-28 16:23:47 -04:00
Max Howell
670dc1163f Merge pull request #68 from mxcl/ci/more 2021-05-28 16:05:44 -04:00
Max Howell
eb33ff8906 [ci] more; some fixes I found 2021-05-28 16:01:57 -04:00
Max Howell
f9cee2c75f Merge pull request #67
[ci] fix
2021-04-30 08:20:56 -04:00
Max Howell
7a974911d8 [ci] fix 2021-04-30 08:06:10 -04:00
Max Howell
891d70ec7c Specify Swift 5.1 syntax for targets 2020-08-30 16:27:17 -04:00
Max Howell
142d4bc111 Merge pull request #64 from mxcl/Path.source()
Add `Path.source()`
2020-08-19 13:41:50 -04:00
Max Howell
6461a550c6 Add Path.source() 2020-08-19 13:27:40 -04:00
Max Howell
8b90260517 Merge pull request #63 from mxcl/codecov-linux
Code coverage for linux
2020-08-01 19:45:07 -04:00
Max Howell
7924d20c8c Code coverage for linux 2020-08-01 15:59:54 -04:00
Max Howell
07007a5421 GHA CI Badge 2020-07-26 13:59:53 -04:00
Max Howell
8a217b3982 Merge pull request #62 from mxcl/sort-ls
ls() is sorted; Fixes #58
2020-07-26 13:58:44 -04:00
Max Howell
2b50909946 ls() is sorted; Fixes #58 2020-07-26 13:48:15 -04:00
Max Howell
baa6416208 Use GHA instead of Travis where possible 2020-07-26 13:39:10 -04:00
Max Howell
6e99825d9f Probably redundant tests, but why not 2020-02-09 14:52:49 -05:00
Max Howell
6e37bfde4d Update README.md 2020-02-09 14:29:28 -05:00
Max Howell
6e1eeb158a No fatal on linux Swift < 5 2020-01-25 14:00:59 -05:00
Max Howell
260196a27a CodeCoverage++ 2020-01-25 14:00:59 -05:00
Max Howell
0de9715b46 [ci skip] Update README.md 2020-01-25 12:05:58 -05:00
Max Howell
240d699986 Delete pushed version tag on failed deploy 2020-01-25 10:53:23 -05:00
Max Howell
b63b5746dc Delete pushed version tag on failed deploy 2020-01-24 12:14:03 -05:00
Max Howell
f062ed9ce3 Fix CI deploy 2020-01-24 12:01:34 -05:00
Max Howell
f1f7ee33b1 [ci skip] List all support Swift 2020-01-24 11:17:42 -05:00
Max Howell
694d04f18b Prepare 1.0.0 release 2020-01-24 11:03:07 -05:00
Max Howell
5636a7ac65 Update Swift Linux test versions 2020-01-18 12:10:32 -05:00
Max Howell
3e964833ff Fix README documentation for Finder 2020-01-18 12:10:32 -05:00
16 changed files with 587 additions and 179 deletions

3
.github/codecov.yml vendored
View File

@@ -1,2 +1,3 @@
ignore: ignore:
- Tests - Tests/PathTests/etc.swift
- Tests/PathTests/TemporaryDirectory.swift

24
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: CD
on:
release:
types: published
jobs:
docs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: steven0351/publish-jazzy-docs@v1
with:
personal_access_token: ${{ secrets.PAT }}
config: .github/jazzy.yml
version: ${{ github.event.release.tag_name }}
history: false
pods:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- run: pod trunk push --allow-warnings
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
VERSION: ${{ github.event.release.tag_name }}

15
.github/workflows/checks.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Checks
on:
push:
branches:
- master
paths:
- Sources/**
- Tests/**
jobs:
macOS:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- run: swift --version
- run: swift test --parallel

83
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: CI
on: pull_request
jobs:
smoke:
runs-on: macos-latest
steps:
- uses: technote-space/auto-cancel-redundant-job@v1
- uses: actions/checkout@v2
- run: swift test --generate-linuxmain
- run: git diff --exit-code
apple:
runs-on: macos-latest
strategy:
matrix:
destination:
- platform=iOS Simulator,OS=latest,name=iPhone 12
- platform=tvOS Simulator,OS=latest,name=Apple TV
# - platform=watchOS Simulator,OS=latest,name=Apple Watch Series 5 - 40mm
# ^^ coming with Xcode 12.5 which is not yet available on GHA
- platform=macOS
steps:
- uses: actions/checkout@v2
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 12.4
- run: swift package generate-xcodeproj --enable-code-coverage
- uses: sersoft-gmbh/xcodebuild-action@v1
with:
project: Path.swift.xcodeproj
scheme: Path.swift-Package
destination: ${{ matrix.destination }}
action: test
- uses: codecov/codecov-action@v1
linux-swift-4:
name: linux (4.2)
runs-on: ubuntu-18.04
steps:
- uses: fwal/setup-swift@v1
with:
swift-version: 4.2
- uses: actions/checkout@v2
- run: swift test --parallel
linux:
runs-on: ubuntu-18.04
strategy:
matrix:
swift:
- '5.0'
- '5.1'
- '5.2'
- '5.3'
steps:
- uses: fwal/setup-swift@v1
with:
swift-version: ${{ matrix.swift }}
- uses: actions/checkout@v2
- run: swift test --parallel --enable-code-coverage
- name: Generate Coverage Report
run: |
sudo apt-get -qq update && sudo apt-get -qq install llvm-10
export b=$(swift build --show-bin-path) && llvm-cov-10 \
export -format lcov \
-instr-profile=$b/codecov/default.profdata \
--ignore-filename-regex='\.build/' \
$b/Path.swiftPackageTests.xctest \
> info.lcov
- uses: codecov/codecov-action@v1
with:
file: ./info.lcov
# code coverage fails with 5.4 for some reason
linux-swift-5-4:
name: linux (5.4)
runs-on: ubuntu-18.04
steps:
- uses: fwal/setup-swift@v1
with:
swift-version: 5.4
- uses: actions/checkout@v2
- run: swift test --parallel

View File

@@ -1,85 +1,37 @@
# only run for: merge commits, releases and pull-requests # Travis does CD, GHA does CI
if: type != push OR branch = master OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/ if: branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/ OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/
stages: stages:
- name: pretest
- name: test
- name: deploy - name: deploy
if: branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/
- name: publish
if: branch =~ /^\d+\.\d+\.\d+(-.*)?$/ if: branch =~ /^\d+\.\d+\.\d+(-.*)?$/
os: osx os: osx
language: swift
osx_image: xcode10.1
xcode_project: Path.swift.xcodeproj
xcode_scheme: Path.swift-Package
jobs: jobs:
include: include:
- name: macOS / Swift 4.0.3
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel -Xswiftc -swift-version -Xswiftc 4
- &std
name: macOS / Swift 4.2.1
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel
- <<: *std
name: macOS / Swift 5.0
osx_image: xcode10.2
- <<: *std
name: macOS / Swift 5.1
osx_image: xcode11
- &xcodebuild
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
- <<: *xcodebuild
name: watchOS / Swift 4.2.1
script: |
set -o pipefail
xcodebuild \
-project Path.swift.xcodeproj \
-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.4
os: linux
name: Linux / Swift 4.2.4
language: generic
sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel
- <<: *linux
env: SWIFT_VERSION='5.0.2'
name: Linux / Swift 5.0.2
- <<: *linux
env: SWIFT_VERSION=5.1-DEVELOPMENT-SNAPSHOT-2019-07-03-a
name: Linux / Swift 5.1 (2019-07-03)
- stage: pretest
name: Check Linux tests are syncd
install: swift test --generate-linuxmain
script: git diff --exit-code
osx_image: xcode10.2
- stage: deploy - stage: deploy
name: Deploy
osx_image: xcode11
env: HOMEBREW_NO_INSTALL_CLEANUP=1
install: brew install mxcl/made/swift-sh
git.depth: false
script:
- set -e
- export VERSION=$(echo $TRAVIS_TAG | cut -c 8-)
- git tag "$VERSION" --force
- git remote set-url origin "https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git"
- git push origin "$VERSION"
- swift sh <(curl https://raw.githubusercontent.com/mxcl/ops/master/deploy) publish-release
- git push origin :$TRAVIS_TAG
after_failure: |
export VERSION=$(echo $TRAVIS_TAG | cut -c 8-)
git push origin :$VERSION
- stage: publish
name: Jazzy name: Jazzy
osx_image: xcode10.2 osx_image: xcode11
install: gem install jazzy install: gem install jazzy
before_script: swift package generate-xcodeproj before_script: swift package generate-xcodeproj
script: | script: |
@@ -95,11 +47,11 @@ jobs:
tags: true tags: true
- name: CocoaPods - name: CocoaPods
osx_image: xcode10.2 env: HOMEBREW_NO_INSTALL_CLEANUP=1
install: | osx_image: xcode11
brew install mxcl/made/swift-sh install:
curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy - brew install mxcl/made/swift-sh
chmod u+x deploy - curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
- chmod u+x deploy
before_script: ./deploy generate-podspec before_script: ./deploy generate-podspec
script: pod trunk push script: pod trunk push
after_success: ./deploy publish-release

16
Path.swift.podspec Normal file
View File

@@ -0,0 +1,16 @@
Pod::Spec.new do |spec|
spec.name = "Path.swift"
spec.version = ENV['VERSION'] || "0.0.1"
spec.summary = "Delightful, robust, cross-platform and chainable file-pathing functions."
spec.homepage = "https://github.com/mxcl/Path.swift"
spec.license = "Unlicense"
spec.author = { "Max Howell" => "mxcl@me.com" }
spec.source = { :git => "https://github.com/mxcl/Path.swift.git", :tag => "#{spec.version}" }
spec.source_files = "Sources/*.swift"
spec.swift_versions = ['4.2', '5']
spec.module_name = 'Path'
spec.osx.deployment_target = '10.10'
spec.ios.deployment_target = '8.0'
spec.tvos.deployment_target = '9.0'
spec.watchos.deployment_target = '2.0'
end

186
README.md
View File

@@ -31,6 +31,9 @@ print(bar.isFile) // => true
let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir()) let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
print(foo) // => /bar/foo print(foo) // => /bar/foo
print(foo.isFile) // => true print(foo.isFile) // => true
// ^^ the `into:` version will only copy *into* a directory, the `to:` version copies
// to a file at that path, thus you will not accidentally copy into directories you
// may not have realized existed.
// we support dynamic-member-syntax when joining named static members, eg: // we support dynamic-member-syntax when joining named static members, eg:
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
@@ -42,18 +45,13 @@ try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
We emphasize safety and correctness, just like Swift, and also (again like We emphasize safety and correctness, just like Swift, and also (again like
Swift), we provide a thoughtful and comprehensive (yet concise) API. Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Sponsor @mxcl
Hi, Im Max Howell and I have written a lot of open source software, and Hi, Im Max Howell and I have written a lot of open source software—generally
probably you already use some of it (Homebrew anyone?). I work full-time on a good deal of my free time 👨🏻‍💻. Sponsorship helps me justify creating new open
open source and its hard; currently I earn *less* than minimum wage. Please source and maintaining it. Thank you.
help me continue my work, I appreciate it x
<a href="https://www.patreon.com/mxcl"> [Sponsor @mxcl].
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
[Other donation/tipping options](http://mxcl.dev/#donate)
# Handbook # Handbook
@@ -75,30 +73,36 @@ try JSONEncoder().encode([Path.home, Path.home/"foo"])
] ]
``` ```
However, often you want to encode relative paths: Though we recommend encoding *relative* paths:
```swift ```swift
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo"]) encoder.encode([Path.home, Path.home/"foo", Path.home/"../baz"])
``` ```
```json ```json
[ [
"", "",
"foo", "foo",
"../baz"
] ]
``` ```
**Note** make sure you decode with this key set *also*, otherwise we `fatal` **Note** if you encode with this key set you *must* decode with the key
(unless the paths are absolute obv.) set also:
```swift ```swift
let decoder = JSONDecoder() let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data) try decoder.decode(from: data) // would throw if `.relativePath` not set
``` ```
> ‡ If you are saving files to a system provided location, eg. Documents then
> the directory could change at Apples choice, or if say the user changes their
> username. Using relative paths also provides you with the flexibility in
> future to change where you are storing your files without hassle.
## Dynamic members ## Dynamic members
We support `@dynamicMemberLookup`: We support `@dynamicMemberLookup`:
@@ -111,7 +115,26 @@ We only provide this for “starting” function, eg. `Path.home` or `Bundle.pat
This is because we found in practice it was easy to write incorrect code, since This is because we found in practice it was easy to write incorrect code, since
everything would compile if we allowed arbituary variables to take *any* named everything would compile if we allowed arbituary variables to take *any* named
property as valid syntax. What we have is what you want most of the time but property as valid syntax. What we have is what you want most of the time but
much less dangerous. much less (potentially) dangerous (at runtime).
### Pathish
`Path`, and `DynamicPath` (the result of eg. `Path.root`) both conform to
`Pathish` which is a protocol that contains all pathing functions. Thus if
you create objects from a mixture of both you need to create generic
functions or convert any `DynamicPath`s to `Path` first:
```swift
let path1 = Path("/usr/lib")!
let path2 = Path.root.usr.bin
var paths = [Path]()
paths.append(path1) // fine
paths.append(path2) // error
paths.append(Path(path2)) // ok
```
This is inconvenient but as Swift stands theres nothing we can think of
that would help.
## Initializing from user-input ## Initializing from user-input
@@ -129,6 +152,28 @@ expect to be relative.
Our initializer is nameless to be consistent with the equivalent operation for Our initializer is nameless to be consistent with the equivalent operation for
converting strings to `Int`, `Float` etc. in the standard library. converting strings to `Int`, `Float` etc. in the standard library.
## Initializing from known strings
Theres no need to use the optional initializer in general if you have known
strings that you need to be paths:
```swift
let absolutePath = "/known/path"
let path1 = Path.root/absolutePath
let pathWithoutInitialSlash = "known/path"
let path2 = Path.root/pathWithoutInitialSlash
assert(path1 == path2)
let path3 = Path(absolutePath)! // at your options
assert(path2 == path3)
// be cautious:
let path4 = Path(pathWithoutInitialSlash)! // CRASH!
```
## Extensions ## Extensions
We have some extensions to Apple APIs: We have some extensions to Apple APIs:
@@ -170,38 +215,46 @@ let files = Path.home.ls().files
// ^^ files that both *exist* and are *not* directories // ^^ files that both *exist* and are *not* directories
let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" } let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }
let includingHiddenFiles = Path.home.ls(.a)
``` ```
**Note** `ls()` does not throw, instead outputing a warning to the console if it
fails to list the directory. The rationale for this is weak, please open a
ticket for discussion.
We provide `find()` for recursive listing: We provide `find()` for recursive listing:
```swift ```swift
Path.home.find().execute { path in for path in Path.home.find() {
// descends all directories, and includes hidden files
// so it behaves the same as the terminal command `find`
}
```
It is configurable:
```swift
for path in Path.home.find().depth(max: 1).extension("swift").type(.file) {
// //
} }
``` ```
Which is configurable: It can be controlled with a closure syntax:
```swift ```swift
Path.home.find().maxDepth(1).extension("swift").kind(.file) { path in Path.home.find().depth(2...3).execute { path in
guard path.basename() != "foo.lock" else { return .abort }
if path.basename() == ".build", path.isDirectory { return .skip }
// //
}
```
And can be controlled:
```swift
Path.home.find().execute { path in
guard foo else { return .skip }
guard bar else { return .abort }
return .continue return .continue
} }
``` ```
Or just get all paths at once: Or get everything at once as an array:
```swift ```swift
let paths = Path.home.find().execute() let paths = Path.home.find().map(\.self)
``` ```
# `Path.swift` is robust # `Path.swift` is robust
@@ -210,8 +263,8 @@ Some parts of `FileManager` are not exactly idiomatic. For example
`isExecutableFile` returns `true` even if there is no file there, it is instead `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 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 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 `isExecutableFile`. `Path.swift` has done the leg-work for you so you can just
with your work without worries. get on with it and not have to worry.
There is also some magic going on in Foundations filesystem APIs, which we look There is also some magic going on in Foundations filesystem APIs, which we look
for and ensure our API is deterministic, eg. [this test]. for and ensure our API is deterministic, eg. [this test].
@@ -225,7 +278,8 @@ round them where necessary.
# Rules & Caveats # Rules & Caveats
Paths are just string representations, there *might not* be a real file there. Paths are just (normalized) string representations, there *might not* be a real
file there.
```swift ```swift
Path.home/"b" // => /Users/mxcl/b Path.home/"b" // => /Users/mxcl/b
@@ -233,7 +287,7 @@ Path.home/"b" // => /Users/mxcl/b
// joining multiple strings works as youd expect // joining multiple strings works as youd expect
Path.home/"b"/"c" // => /Users/mxcl/b/c Path.home/"b"/"c" // => /Users/mxcl/b/c
// joining multiple parts at a time is fine // joining multiple parts simultaneously is fine
Path.home/"b/c" // => /Users/mxcl/b/c Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash // joining with absolute paths omits prefixed slash
@@ -243,6 +297,9 @@ Path.home/"/b" // => /Users/mxcl/b
Path.home.foo.bar.join("..") // => /Users/mxcl/foo Path.home.foo.bar.join("..") // => /Users/mxcl/foo
Path.home.foo.bar.join(".") // => /Users/mxcl/foo/bar Path.home.foo.bar.join(".") // => /Users/mxcl/foo/bar
// though note that we provide `.parent`:
Path.home.foo.bar.parent // => /Users/mxcl/foo
// of course, feel free to join variables: // of course, feel free to join variables:
let b = "b" let b = "b"
let c = "c" let c = "c"
@@ -266,11 +323,11 @@ Path("/foo/bar/../baz") // => /foo/baz
// symlinks are not resolved // symlinks are not resolved
Path.root.bar.symlink(as: "foo") Path.root.bar.symlink(as: "foo")
Path("foo") // => /foo Path("/foo") // => /foo
Path.foo // => /foo Path.root.foo // => /foo
// unless you do it explicitly // unless you do it explicitly
try Path.foo.readlink() // => /bar try Path.root.foo.readlink() // => /bar
// `readlink` only resolves the *final* path component, // `readlink` only resolves the *final* path component,
// thus use `realpath` if there are multiple symlinks // thus use `realpath` if there are multiple symlinks
``` ```
@@ -282,7 +339,7 @@ then its a noop:
* If you try to make a directory and it already exists, we do nothing. * If you try to make a directory and it already exists, we do nothing.
* If you call `readlink` on a non-symlink, we return `self` * If you call `readlink` on a non-symlink, we return `self`
However notably if you try to copy or move a file with specifying `overwrite` However notably if you try to copy or move a file without specifying `overwrite`
and the file already exists at the destination and is identical, we dont check and the file already exists at the destination and is identical, we dont check
for that as the check was deemed too expensive to be worthwhile. for that as the check was deemed too expensive to be worthwhile.
@@ -293,23 +350,27 @@ for that as the check was deemed too expensive to be worthwhile.
equality check is required. equality check is required.
* There are several symlink paths on Mac that are typically automatically * There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for resolved by Foundation, eg. `/private`, we attempt to do the same for
functions that you would expect it (notably `realpath`), we *do* the same for functions that you would expect it (notably `realpath`), we *do* the same
`Path.init`, but *do not* if you are joining a path that ends up being one of for `Path.init`, but *do not* if you are joining a path that ends up being
these paths, (eg. `Path.root.join("var/private')`). one of these paths, (eg. `Path.root.join("var/private')`).
If a `Path` is a symlink but the destination of the link does not exist `exists` If a `Path` is a symlink but the destination of the link does not exist `exists`
returns `false`. This seems to be the correct thing to do since symlinks are returns `false`. This seems to be the correct thing to do since symlinks are
meant to be an abstraction for filesystems. To instead verify that there is meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`. no filesystem entry there at all check if `type` is `nil`.
## We do not provide change directory functionality ## We do not provide change directory functionality
Changing directory is dangerous, you should *always* try to avoid it and thus Changing directory is dangerous, you should *always* try to avoid it and thus
we dont even provide the method. If you are executing a sub-process then we dont even provide the method. If you are executing a sub-process then
use `Process.currentDirectoryURL`. use `Process.currentDirectoryURL` to change *its* working directory when it
executes.
If you must then use `FileManager.changeCurrentDirectory`. If you must change directory then use `FileManager.changeCurrentDirectory` as
early in your process as *possible*. Altering the global state of your apps
environment is fundamentally dangerous creating hard to debug issues that
you wont find for potentially *years*.
# I thought I should only use `URL`s? # I thought I should only use `URL`s?
@@ -318,8 +379,8 @@ Apple recommend this because they provide a magic translation for
file:///.file/id=6571367.15106761 file:///.file/id=6571367.15106761
Therefore, if you are not using this feature you are fine. If you have URLs the correct Therefore, if you are not using this feature you are fine. If you have URLs the
way to get a `Path` is: correct way to get a `Path` is:
```swift ```swift
if let path = Path(url: url) { if let path = Path(url: url) {
@@ -332,33 +393,39 @@ actual filesystem path, however we also check the URL has a `file` scheme first.
[file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl [file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
# In defense of our naming scheme
Chainable syntax demands short method names, thus we adopted the naming scheme
of the terminal, which is absolutely not very “Apple” when it comes to how they
design their APIs, however for users of the terminal (which *surely* is most
developers) it is snappy and familiar.
# Installation # Installation
SwiftPM: SwiftPM:
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0")) package.append(
.package(url: "https://github.com/mxcl/Path.swift.git", from: "1.0.0")
)
package.targets.append(
.target(name: "Foo", dependencies: [
.product(name: "Path", package: "Path.swift")
])
)
``` ```
CocoaPods: CocoaPods:
```ruby ```ruby
pod 'Path.swift', '~> 0.13' pod 'Path.swift', '~> 1.0.0'
``` ```
Carthage: Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945). > Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Pre1.0 status
We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### Get push notifications for new releases
https://mxcl.dev/canopy/
# Alternatives # Alternatives
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan * [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
@@ -368,12 +435,13 @@ https://mxcl.dev/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-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 [badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.x-orange.svg
[docs]: https://mxcl.dev/Path.swift/Structs/Path.html [docs]: https://mxcl.dev/Path.swift/Structs/Path.html
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true [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-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg
[badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg [badge-ci]: https://github.com/mxcl/Path.swift/workflows/Checks/badge.svg
[travis]: https://travis-ci.com/mxcl/Path.swift [travis]: https://travis-ci.com/mxcl/Path.swift
[codecov]: https://codecov.io/gh/mxcl/Path.swift [codecov]: https://codecov.io/gh/mxcl/Path.swift
[badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version [badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version
[cocoapods]: https://cocoapods.org/pods/Path.swift [cocoapods]: https://cocoapods.org/pods/Path.swift
[Sponsor @mxcl]: https://github.com/sponsors/mxcl

View File

@@ -14,6 +14,18 @@ extension Path {
return .init(string: "/") return .init(string: "/")
} }
#if swift(>=5.3)
public static func source(for filePath: String = #filePath) -> (file: DynamicPath, directory: DynamicPath) {
let file = DynamicPath(string: filePath)
return (file: file, directory: .init(file.parent))
}
#else
public static func source(for filePath: String = #file) -> (file: DynamicPath, directory: DynamicPath) {
let file = DynamicPath(string: filePath)
return (file: file, directory: .init(file.parent))
}
#endif
/// Returns a `Path` representing the users home directory /// Returns a `Path` representing the users home directory
public static var home: DynamicPath { public static var home: DynamicPath {
let string: String let string: String

View File

@@ -29,7 +29,8 @@ public extension Pathish {
if overwrite, let tokind = to.type, tokind != .directory, type != .directory { if overwrite, let tokind = to.type, tokind != .directory, type != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
#if os(Linux) && !swift(>=5.2) // check if fixed #if os(Linux)
//NOTE doing manually due to inconsistency in Linux Foundation behavior
if !overwrite, to.type != nil { if !overwrite, to.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
@@ -69,7 +70,8 @@ public extension Pathish {
if overwrite, let kind = rv.type, kind != .directory { if overwrite, let kind = rv.type, kind != .directory {
try FileManager.default.removeItem(at: rv.url) try FileManager.default.removeItem(at: rv.url)
} }
#if os(Linux) && !swift(>=5.2) // check if fixed #if os(Linux)
//NOTE doing manually due to inconsistency in Linux Foundation behavior
if !overwrite, rv.type != nil { if !overwrite, rv.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
@@ -204,7 +206,7 @@ public extension Pathish {
} }
/** /**
Renames the file at path. Renames the file (basename only) at path.
Path.root.foo.bar.rename(to: "baz") // => /foo/baz Path.root.foo.bar.rename(to: "baz") // => /foo/baz

View File

@@ -14,7 +14,13 @@ public extension Path {
private let enumerator: FileManager.DirectoryEnumerator! private let enumerator: FileManager.DirectoryEnumerator!
/// The range of directory depths for which the find operation will return entries. /// The range of directory depths for which the find operation will return entries.
private(set) public var depth: ClosedRange<Int> = 1...Int.max private(set) public var depth: ClosedRange<Int> = 1...Int.max {
didSet {
if depth.lowerBound < 0 {
depth = 0...depth.upperBound
}
}
}
/// The kinds of filesystem entries find operations will return. /// The kinds of filesystem entries find operations will return.
public var types: Set<EntryType> { public var types: Set<EntryType> {
@@ -42,12 +48,8 @@ extension Path.Finder: Sequence, IteratorProtocol {
continue continue
} }
if enumerator.level < depth.lowerBound { if enumerator.level < depth.lowerBound {
if path == self.path, depth.lowerBound == 0 {
return path
} else {
continue continue
} }
}
#endif #endif
if let type = path.type, !types.contains(type) { continue } if let type = path.type, !types.contains(type) { continue }
@@ -128,7 +130,11 @@ public extension Path.Finder {
while let path = next() { while let path = next() {
switch try closure(path) { switch try closure(path) {
case .skip: case .skip:
#if !os(Linux) || swift(>=5.0)
enumerator.skipDescendants() enumerator.skipDescendants()
#else
fputs("warning: skip is not implemented for Swift < 5.0\n", stderr)
#endif
case .abort: case .abort:
return return
case .continue: case .continue:
@@ -158,7 +164,7 @@ public extension Pathish {
if options != .a, path.basename().hasPrefix(".") { return nil } if options != .a, path.basename().hasPrefix(".") { return nil }
// ^^ we dont use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls` // ^^ we dont use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls`
return path return path
} }.sorted()
} }
/// Recursively find files under this path. If the path is a file, no files will be found. /// Recursively find files under this path. If the path is a file, no files will be found.

View File

@@ -11,7 +11,8 @@ public extension Pathish {
/** /**
- Returns: `true` if the path represents an actual filesystem entry. - Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination. - Note: If `self` is a symlink the return value represents the destination, thus if the destination doesnt exist this returns `false` even if the symlink exists.
- Note: To determine if a symlink exists, use `.type`.
*/ */
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)

View File

@@ -168,6 +168,7 @@ public extension Pathish {
*/ */
var parent: Path { var parent: Path {
let index = string.lastIndex(of: "/")! let index = string.lastIndex(of: "/")!
guard index != string.indices.startIndex else { return Path(string: "/") }
let substr = string[string.indices.startIndex..<index] let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr)) return Path(string: String(substr))
} }
@@ -176,7 +177,7 @@ public extension Pathish {
Returns the filename extension of this path. Returns the filename extension of this path.
- Remark: If there is no extension returns "". - Remark: If there is no extension returns "".
- Remark: If the filename ends with any number of ".", returns "". - Remark: If the filename ends with any number of ".", returns "".
- Note: We special case eg. `foo.tar.gz`. - Note: We special case eg. `foo.tar.gz`there are a limited number of these specializations, feel free to PR more.
*/ */
@inlinable @inlinable
var `extension`: String { var `extension`: String {

View File

@@ -52,6 +52,90 @@ extension PathTests {
} }
} }
func testFindMinDepth() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let finder = tmpdir.find().depth(min: 2)
XCTAssertEqual(finder.depth, 2...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindDepth0() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let finder = tmpdir.find().depth(min: 0)
XCTAssertEqual(finder.depth, 0...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
do {
// this should work, even though its weird
let finder = tmpdir.find().depth(min: -1)
XCTAssertEqual(finder.depth, 0...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindDepthRange() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let range = 2...3
let finder = tmpdir.find().depth(range)
XCTAssertEqual(finder.depth, range)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e, tmpdir.b.d.f].map(Path.init)),
relativeTo: tmpdir)
#endif
}
do {
let range = 2..<4
let finder = tmpdir.find().depth(range)
XCTAssertEqual(finder.depth, 2...3)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e, tmpdir.b.d.f].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindExtension() throws { func testFindExtension() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch() try tmpdir.join("foo.json").touch()
@@ -66,7 +150,55 @@ extension PathTests {
} }
} }
func testFindKinds() throws { //NOTE this is how iterators work, so we have a test to validate that behavior
func testFindCallingExecuteTwice() throws {
try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch()
try tmpdir.join("bar.txt").touch()
let finder = tmpdir.find()
XCTAssertEqual(finder.map{ $0 }.count, 2)
XCTAssertEqual(finder.map{ $0 }.count, 0)
}
}
func testFindExecute() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
#if !os(Linux) || swift(>=5)
do {
var rv = Set<Path>()
tmpdir.find().execute {
switch $0 {
case Path(tmpdir.b.d): return .skip
default:
rv.insert($0)
return .continue
}
}
XCTAssertEqual(rv, Set([tmpdir.a, tmpdir.b, tmpdir.b.c].map(Path.init)))
}
#endif
do {
var x = 0
tmpdir.find().execute { _ in
x += 1
return .abort
}
XCTAssertEqual(x, 1)
}
}
}
func testFindTypes() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
try tmpdir.foo.mkdir() try tmpdir.foo.mkdir()
try tmpdir.bar.touch() try tmpdir.bar.touch()
@@ -82,4 +214,16 @@ extension PathTests {
Set(["foo", "bar"].map(tmpdir.join))) Set(["foo", "bar"].map(tmpdir.join)))
} }
} }
func testLsOnNonexistentDirectoryReturnsEmptyArray() throws {
try Path.mktemp { tmpdir in
XCTAssertEqual(tmpdir.a.ls(), [])
}
}
func testFindOnNonexistentDirectoryHasNoContent() throws {
try Path.mktemp { tmpdir in
XCTAssertNil(tmpdir.a.find().next())
}
}
} }

View File

@@ -146,15 +146,21 @@ class PathTests: XCTestCase {
].map(Path.init) ].map(Path.init)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root
func test<P: Pathish>(relativePath: P, line: UInt = #line) throws {
encoder.userInfo[.relativePath] = relativePath
let data = try encoder.encode(input) let data = try encoder.encode(input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"]) XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"], line: line)
let decoder = JSONDecoder() let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode([Path].self, from: data)) XCTAssertThrowsError(try decoder.decode([Path].self, from: data), line: line)
decoder.userInfo[.relativePath] = root decoder.userInfo[.relativePath] = relativePath
XCTAssertEqual(try decoder.decode([Path].self, from: data), input) XCTAssertEqual(try decoder.decode([Path].self, from: data), input, line: line)
}
try test(relativePath: root) // DynamicPath
try test(relativePath: Path(root)) // Path
} }
func testJoin() { func testJoin() {
@@ -186,6 +192,14 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar") XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar")
} }
func testParent() {
XCTAssertEqual(Path("/root/boot")!.parent.string, "/root")
XCTAssertEqual(Path("/root/boot")!.parent.parent.string, "/")
XCTAssertEqual(Path("/root/boot")!.parent.parent.parent.string, "/")
XCTAssertEqual(Path("/root")!.parent.string, "/")
XCTAssertEqual(Path("/root")!.parent.parent.string, "/")
}
func testDynamicMember() { func testDynamicMember() {
XCTAssertEqual(Path.root.Documents, Path.root/"Documents") XCTAssertEqual(Path.root.Documents, Path.root/"Documents")
@@ -193,7 +207,7 @@ class PathTests: XCTestCase {
XCTAssertEqual(a.Documents, Path.home/"foo/Documents") XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
// verify use of the dynamic-member-subscript works according to our rules // verify use of the dynamic-member-subscript works according to our rules
XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, "\(Path.home.parent.string)/~foo") XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, Path(Path.home).parent.join("~foo").string)
} }
func testCopyTo() throws { func testCopyTo() throws {
@@ -299,6 +313,13 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root.string, "/") XCTAssertEqual(Path.root.string, "/")
XCTAssertEqual(Path.home.string, NSHomeDirectory()) XCTAssertEqual(Path.home.string, NSHomeDirectory())
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents") XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
#if swift(>=5.3)
let filePath = Path(#filePath)!
#else
let filePath = Path(#file)!
#endif
XCTAssertEqual(Path.source().file, filePath)
XCTAssertEqual(Path.source().directory, filePath.parent)
#if !os(Linux) #if !os(Linux)
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches") XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath) XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
@@ -313,6 +334,8 @@ class PathTests: XCTestCase {
func testStringConvertibles() { func testStringConvertibles() {
XCTAssertEqual(Path.root.description, "/") XCTAssertEqual(Path.root.description, "/")
XCTAssertEqual(Path.root.debugDescription, "Path(/)") XCTAssertEqual(Path.root.debugDescription, "Path(/)")
XCTAssertEqual(Path(Path.root).description, "/")
XCTAssertEqual(Path(Path.root).debugDescription, "Path(/)")
} }
func testFilesystemAttributes() throws { func testFilesystemAttributes() throws {
@@ -365,17 +388,13 @@ class PathTests: XCTestCase {
func testTimes() throws { func testTimes() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let now1 = Date().timeIntervalSince1970.rounded(.down) 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) sleep(1)
try foo.touch() let foo = try tmpdir.foo.touch()
let now2 = Date().timeIntervalSince1970.rounded(.down) #if !os(Linux)
XCTAssertNotEqual(now1, now2) XCTAssertGreaterThan(foo.ctime?.timeIntervalSince1970.rounded(.down) ?? 0, now1) //FIXME flakey
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now2) //FIXME flakey #endif
XCTAssertGreaterThan(foo.mtime?.timeIntervalSince1970.rounded(.down) ?? 0, now1) //FIXME flakey
XCTAssertNil(tmpdir.void.mtime) XCTAssertNil(tmpdir.void.mtime)
XCTAssertNil(tmpdir.void.ctime) XCTAssertNil(tmpdir.void.ctime)
@@ -405,6 +424,9 @@ class PathTests: XCTestCase {
XCTAssertNoThrow(try bar7.delete()) XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.type, nil) XCTAssertEqual(bar7.type, nil)
XCTAssertEqual(tmpdir.bar6.type, .file) XCTAssertEqual(tmpdir.bar6.type, .file)
// for code-coverage
XCTAssertEqual(tmpdir.bar6.kind, .file)
} }
} }
@@ -436,7 +458,7 @@ class PathTests: XCTestCase {
#if os(macOS) #if os(macOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks) XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources) XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources)
#elseif os(tvOS) || os(iOS) #elseif os(tvOS) || os(iOS) || os(watchOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks) XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir) XCTAssertEqual(bndl.defaultResourcesPath, tmpdir)
#else #else
@@ -488,8 +510,19 @@ class PathTests: XCTestCase {
func testTouchThrowsIfCannotWrite() throws { func testTouchThrowsIfCannotWrite() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
print(try FileManager.default.attributesOfItem(atPath: tmpdir.string)[.posixPermissions])
//FIXME fails in Docker image (only)
try tmpdir.chmod(0o000) try tmpdir.chmod(0o000)
let attrs = try FileManager.default.attributesOfItem(atPath: tmpdir.string)
XCTAssertEqual(attrs[.posixPermissions] as? Int, 0)
print(attrs[.posixPermissions])
XCTAssertThrowsError(try tmpdir.bar.touch()) XCTAssertThrowsError(try tmpdir.bar.touch())
XCTAssertFalse(tmpdir.bar.exists)
} }
} }
@@ -642,12 +675,10 @@ class PathTests: XCTestCase {
XCTAssertEqual(bar.type, .symlink) XCTAssertEqual(bar.type, .symlink)
} }
} }
}
private func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #file, line: UInt = #line) { func testOptionalInitializer() throws {
XCTAssertEqual(p.string, q.string, file: file, line: line) XCTAssertNil(Path(""))
} XCTAssertNil(Path("./foo"))
XCTAssertEqual(Path("/foo"), Path.root.foo)
private func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P?, _ q: Q?, file: StaticString = #file, line: UInt = #line) { }
XCTAssertEqual(p?.string, q?.string, file: file, line: line)
} }

View File

@@ -24,21 +24,30 @@ extension PathTests {
("testFileHandleExtensions", testFileHandleExtensions), ("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference), ("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes), ("testFilesystemAttributes", testFilesystemAttributes),
("testFindCallingExecuteTwice", testFindCallingExecuteTwice),
("testFindDepth0", testFindDepth0),
("testFindDepthRange", testFindDepthRange),
("testFindExecute", testFindExecute),
("testFindExtension", testFindExtension), ("testFindExtension", testFindExtension),
("testFindKinds", testFindKinds),
("testFindMaxDepth1", testFindMaxDepth1), ("testFindMaxDepth1", testFindMaxDepth1),
("testFindMaxDepth2", testFindMaxDepth2), ("testFindMaxDepth2", testFindMaxDepth2),
("testFindMinDepth", testFindMinDepth),
("testFindOnNonexistentDirectoryHasNoContent", testFindOnNonexistentDirectoryHasNoContent),
("testFindTypes", testFindTypes),
("testFlatMap", testFlatMap), ("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath), ("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testKind", testKind), ("testKind", testKind),
("testLock", testLock), ("testLock", testLock),
("testLsOnNonexistentDirectoryReturnsEmptyArray", testLsOnNonexistentDirectoryReturnsEmptyArray),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto), ("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo), ("testMoveTo", testMoveTo),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution), ("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testOptionalInitializer", testOptionalInitializer),
("testParent", testParent),
("testPathComponents", testPathComponents), ("testPathComponents", testPathComponents),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf), ("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows), ("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),

43
Tests/PathTests/etc.swift Normal file
View File

@@ -0,0 +1,43 @@
import XCTest
import Path
#if swift(>=5.3)
func XCTAssertEqual<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, relativeTo: P) {
logic(set1, set2, relativeTo: relativeTo) {
XCTFail($0, file: file, line: line)
}
}
#else
func XCTAssertEqual<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line, relativeTo: P) {
logic(set1, set2, relativeTo: relativeTo) {
XCTFail($0, file: file, line: line)
}
}
#endif
private func logic<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, relativeTo: P, fail: (String) -> Void) {
if set1 != set2 {
let cvt: (Path) -> String = { $0.relative(to: relativeTo) }
let out1 = set1.map(cvt).sorted()
let out2 = set1.map(cvt).sorted()
fail("Set(\(out1)) is not equal to Set(\(out2))")
}
}
#if swift(>=5.3)
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertEqual(p.string, q.string, file: file, line: line)
}
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P?, _ q: Q?, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertEqual(p?.string, q?.string, file: file, line: line)
}
#else
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(p.string, q.string, file: file, line: line)
}
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P?, _ q: Q?, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(p?.string, q?.string, file: file, line: line)
}
#endif