mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
Apple platform simulator CI failures ### Motivation * Several tests failed when testing against simulators for Apple platforms (iOS, tvOS, watchOS, visionOS) ### Modifications * Added `@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)` to each `@Test` method in `NIOThreadPoolTest`, `EventLoopFutureTest`, and `NIOTransportAccessibleChannelCoreTests` to match the availability of the APIs they use (`timeLimit` and `withUnsafeTransportIfAvailable`). * Removed `.timeLimit(.minutes(1))` from the `@Suite` declarations on `NIOThreadPoolTest` and `EventLoopFutureTest` - the `@Suite` macro cannot be used with `@available`, so `timeLimit` cannot be used. * Skip `testHomeDirectoryFromPasswd` on simulator targets via `#if targetEnvironment(simulator)` / `XCTSkip`, since simulators retu> success but nil information when invoking `getpwuid_r`. ### Result * Apple simulator platform builds and test runs no longer fail due to these issues. Some infrastructure issues remain to be resolved separately.
2581 lines
100 KiB
Swift
2581 lines
100 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import NIOConcurrencyHelpers
|
|
import NIOCore
|
|
@_spi(Testing) @testable import NIOFS
|
|
import SystemPackage
|
|
import XCTest
|
|
|
|
extension NIOFilePath {
|
|
static let testData = NIOFilePath(
|
|
FilePath(#filePath)
|
|
.removingLastComponent() // FileHandleTests.swift
|
|
.appending("Test Data")
|
|
.lexicallyNormalized()
|
|
)
|
|
|
|
static let testDataReadme = NIOFilePath(Self.testData.underlying.appending("README.md"))
|
|
static let testDataReadmeSymlink = NIOFilePath(Self.testData.underlying.appending("README.md.symlink"))
|
|
}
|
|
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
extension FileSystem {
|
|
func temporaryFilePath(
|
|
_ function: String = #function,
|
|
inTemporaryDirectory: Bool = true
|
|
) async throws -> NIOFilePath {
|
|
if inTemporaryDirectory {
|
|
let directory = (try await self.temporaryDirectory).underlying
|
|
return self.temporaryFilePath(function, inDirectory: directory)
|
|
} else {
|
|
return self.temporaryFilePath(function, inDirectory: nil)
|
|
}
|
|
}
|
|
|
|
func temporaryFilePath(
|
|
_ function: String = #function,
|
|
inDirectory directory: FilePath?
|
|
) -> NIOFilePath {
|
|
let index = function.firstIndex(of: "(")!
|
|
let functionName = function.prefix(upTo: index)
|
|
let random = UInt32.random(in: .min ... .max)
|
|
let fileName = "\(functionName)-\(random)"
|
|
|
|
if let directory = directory {
|
|
return NIOFilePath(directory.appending(fileName))
|
|
} else {
|
|
return NIOFilePath(fileName)
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
final class FileSystemTests: XCTestCase {
|
|
var fs: FileSystem { .shared }
|
|
|
|
func testOpenFileForReading() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: .testDataReadme) { file in
|
|
let info = try await file.info()
|
|
XCTAssertEqual(info.type, .regular)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testOpenFileForReadingFollowsSymlink() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: .testDataReadmeSymlink) { file in
|
|
let info = try await file.info()
|
|
XCTAssertEqual(info.type, .regular)
|
|
}
|
|
}
|
|
|
|
func testOpenSymlinkForReadingWithoutFollow() async throws {
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withFileHandle(
|
|
forReadingAt: .testDataReadmeSymlink,
|
|
options: OpenOptions.Read(followSymbolicLinks: false)
|
|
) { _ in
|
|
}
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .invalidArgument)
|
|
}
|
|
}
|
|
|
|
func testOpenNonExistentFileForReading() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withFileHandle(forReadingAt: path) { _ in }
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testOpenFileWhereIntermediateIsNotADirectory() async throws {
|
|
let path = NIOFilePath(FilePath(#filePath).appending("foobar"))
|
|
|
|
// For reading:
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withFileHandle(forReadingAt: path) { _ in
|
|
XCTFail("File unexpectedly opened")
|
|
}
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
|
|
// For writing:
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withFileHandle(forWritingAt: path) { _ in
|
|
XCTFail("File unexpectedly opened")
|
|
}
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
|
|
// As a directory:
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withDirectoryHandle(atPath: path) { _ in
|
|
XCTFail("File unexpectedly opened")
|
|
}
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testOpenFileForWriting() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { file in
|
|
let info = try await file.info()
|
|
XCTAssertEqual(info.type, .regular)
|
|
XCTAssertEqual(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testOpenNonExistentFileForWritingWithoutCreating() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .modifyFile(createIfNecessary: false)
|
|
) { _ in }
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testOpenForWritingFollowingSymlink() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
|
|
let link = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: link, withDestination: path)
|
|
|
|
// Open via the link and write.
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: link,
|
|
options: .modifyFile(createIfNecessary: false)
|
|
) { file in
|
|
let info = try await file.info()
|
|
XCTAssertEqual(info.type, .regular)
|
|
|
|
try await file.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let contents = try await ByteBuffer(
|
|
contentsOf: path,
|
|
maximumSizeAllowed: .bytes(1024),
|
|
fileSystem: self.fs
|
|
)
|
|
XCTAssertEqual(contents, ByteBuffer(bytes: [0, 1, 2]))
|
|
}
|
|
|
|
func testOpenNonExistentFileForWritingWithMaterialization() async throws {
|
|
for isAbsolute in [true, false] {
|
|
let path = try await self.fs.temporaryFilePath(inTemporaryDirectory: isAbsolute)
|
|
XCTAssertEqual(path.underlying.isAbsolute, isAbsolute)
|
|
|
|
await XCTAssertThrowsErrorAsync {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { file in
|
|
// The file hasn't materialized yet, so no file at the expected path
|
|
// should exist.
|
|
let info = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNil(info)
|
|
|
|
try await file.write(
|
|
contentsOf: repeatElement(0, count: 1024),
|
|
toAbsoluteOffset: 0
|
|
)
|
|
throw CancellationError()
|
|
}
|
|
} onError: { error in
|
|
XCTAssert(error is CancellationError)
|
|
}
|
|
|
|
// Threw in the 'with' block; the file shouldn't exist anymore.
|
|
let info = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNil(info)
|
|
}
|
|
}
|
|
|
|
func testOpenExistingFileForWritingWithMaterialization() async throws {
|
|
for isAbsolute in [true, false] {
|
|
let path = try await self.fs.temporaryFilePath(inTemporaryDirectory: isAbsolute)
|
|
XCTAssertEqual(path.underlying.isAbsolute, isAbsolute)
|
|
|
|
// Avoid dirtying the current working directory.
|
|
if path.underlying.isRelative {
|
|
self.addTeardownBlock { [fileSystem = self.fs] in
|
|
try await fileSystem.removeItem(at: path, strategy: .platformDefault)
|
|
}
|
|
}
|
|
|
|
// Create a file and write some data to it.
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { file in
|
|
_ = try await file.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0)
|
|
}
|
|
|
|
// File must exist now.
|
|
let info = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNotNil(info)
|
|
|
|
// Open the existing file and truncate it. Write different bytes to it but then throw an
|
|
// error. The changes shouldn't persist because of the error.
|
|
await XCTAssertThrowsErrorAsync {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: true)
|
|
) { file in
|
|
try await file.write(contentsOf: [3, 4, 5], toAbsoluteOffset: 0)
|
|
throw CancellationError()
|
|
}
|
|
} onError: { error in
|
|
XCTAssert(error is CancellationError)
|
|
}
|
|
|
|
// Read the file again, it should contain the original bytes.
|
|
let bytes = try await ByteBuffer(contentsOf: path, maximumSizeAllowed: .megabytes(1))
|
|
XCTAssertEqual(bytes, ByteBuffer(bytes: [0, 1, 2]))
|
|
}
|
|
}
|
|
|
|
func testDetachUnsafeDescriptorForFileOpenedWithMaterialization() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
let descriptor = try await self.fs.withFileHandle(forWritingAt: path) { handle in
|
|
_ = try await handle.withBufferedWriter { writer in
|
|
try await writer.write(contentsOf: repeatElement(0, count: 1024))
|
|
}
|
|
|
|
return try handle.detachUnsafeFileDescriptor()
|
|
}
|
|
|
|
try descriptor.writeAll(toAbsoluteOffset: 1024, repeatElement(1, count: 1024))
|
|
|
|
var buffer = try await ByteBuffer(
|
|
contentsOf: path,
|
|
maximumSizeAllowed: .mebibytes(2),
|
|
fileSystem: self.fs
|
|
)
|
|
|
|
XCTAssertEqual(buffer.readBytes(length: 1024), Array(repeating: 0, count: 1024))
|
|
XCTAssertEqual(buffer.readBytes(length: 1024), Array(repeating: 1, count: 1024))
|
|
XCTAssertEqual(buffer.readableBytes, 0)
|
|
}
|
|
|
|
func testOpenFileForReadingAndWriting() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forReadingAndWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) {
|
|
file in
|
|
let info = try await file.info()
|
|
XCTAssertEqual(info.type, .regular)
|
|
XCTAssertEqual(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testOpenNonExistentFileForReadingAndWritingWithoutCreating() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withFileHandle(
|
|
forReadingAndWritingAt: path,
|
|
options: .modifyFile(createIfNecessary: false)
|
|
) {
|
|
_ in
|
|
}
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testOpenDirectory() async throws {
|
|
try await self.fs.withDirectoryHandle(atPath: .testData) { dir in
|
|
let info = try await dir.info()
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testOpenNonExistentDirectory() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.withDirectoryHandle(atPath: path) { _ in }
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testOpenDirectoryFollowingSymlink() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
|
|
|
|
let link = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: link, withDestination: path)
|
|
|
|
try await self.fs.withDirectoryHandle(atPath: link) { dir in
|
|
let info = try await dir.info()
|
|
XCTAssertEqual(info.type, .directory)
|
|
}
|
|
}
|
|
|
|
func testOpenNonExistentFileForWritingRelativeToDirectoryWithMaterialization() async throws {
|
|
// (false, false) isn't supported.
|
|
let isPathAbsolute: [(Bool, Bool)] = [(true, true), (true, false), (false, true)]
|
|
|
|
for (isDirectoryAbsolute, isFileAbsolute) in isPathAbsolute {
|
|
let directoryPath = try await self.fs.temporaryFilePath(
|
|
inTemporaryDirectory: isDirectoryAbsolute
|
|
)
|
|
XCTAssertEqual(directoryPath.underlying.isAbsolute, isDirectoryAbsolute)
|
|
|
|
// Avoid dirtying the current working directory.
|
|
if directoryPath.underlying.isRelative {
|
|
self.addTeardownBlock { [fileSystem = self.fs] in
|
|
try await fileSystem.removeItem(at: directoryPath, strategy: .platformDefault)
|
|
}
|
|
}
|
|
|
|
// Create the directory and open it
|
|
try await self.fs.createDirectory(at: directoryPath, withIntermediateDirectories: true)
|
|
try await self.fs.withDirectoryHandle(atPath: directoryPath) { directory in
|
|
let filePath = try await self.fs.temporaryFilePath(
|
|
inTemporaryDirectory: isFileAbsolute
|
|
)
|
|
|
|
XCTAssertEqual(filePath.underlying.isAbsolute, isFileAbsolute)
|
|
|
|
// Create the file and throw.
|
|
await XCTAssertThrowsErrorAsync {
|
|
try await directory.withFileHandle(
|
|
forWritingAt: filePath,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
throw CancellationError()
|
|
}
|
|
} onError: { error in
|
|
XCTAssert(error is CancellationError)
|
|
}
|
|
|
|
// The file shouldn't exist.
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await directory.withFileHandle(forReadingAt: filePath) { _ in }
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testOpenExistingFileForWritingRelativeToDirectoryWithMaterialization() async throws {
|
|
// (false, false) isn't supported.
|
|
let isPathAbsolute: [(Bool, Bool)] = [(true, true), (true, false), (false, true)]
|
|
|
|
for (isDirectoryAbsolute, isFileAbsolute) in isPathAbsolute {
|
|
let directoryPath = try await self.fs.temporaryFilePath(
|
|
inTemporaryDirectory: isDirectoryAbsolute
|
|
)
|
|
|
|
XCTAssertEqual(directoryPath.underlying.isAbsolute, isDirectoryAbsolute)
|
|
|
|
if directoryPath.underlying.isRelative {
|
|
self.addTeardownBlock { [fileSystem = self.fs] in
|
|
try await fileSystem.removeItem(at: directoryPath, strategy: .platformDefault, recursively: true)
|
|
}
|
|
}
|
|
|
|
// Create the directory and open it
|
|
try await self.fs.createDirectory(at: directoryPath, withIntermediateDirectories: true)
|
|
try await self.fs.withDirectoryHandle(atPath: directoryPath) { directory in
|
|
let filePath = try await self.fs.temporaryFilePath(
|
|
inTemporaryDirectory: isFileAbsolute
|
|
)
|
|
|
|
XCTAssertEqual(filePath.underlying.isAbsolute, isFileAbsolute)
|
|
|
|
// Create the file and write some bytes.
|
|
try await directory.withFileHandle(
|
|
forWritingAt: filePath,
|
|
options: .newFile(replaceExisting: false)
|
|
) { file in
|
|
_ = try await file.write(
|
|
contentsOf: repeatElement(0, count: 1024),
|
|
toAbsoluteOffset: 0
|
|
)
|
|
}
|
|
|
|
// Create the file and throw.
|
|
await XCTAssertThrowsErrorAsync {
|
|
try await directory.withFileHandle(
|
|
forWritingAt: filePath,
|
|
options: .newFile(replaceExisting: true)
|
|
) { handle in
|
|
throw CancellationError()
|
|
}
|
|
} onError: { error in
|
|
XCTAssert(error is CancellationError)
|
|
}
|
|
|
|
// The file should contain the original bytes.
|
|
try await directory.withFileHandle(forReadingAt: filePath) { file in
|
|
let bytes = try await file.readToEnd(maximumSizeAllowed: .megabytes(1))
|
|
XCTAssertEqual(bytes, ByteBuffer(repeating: 0, count: 1024))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCreateDirectory() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createDirectory(
|
|
at: path,
|
|
withIntermediateDirectories: false,
|
|
permissions: nil
|
|
)
|
|
|
|
try await self.fs.withDirectoryHandle(atPath: path) { dir in
|
|
let info = try await dir.info()
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testCreateDirectoryWithIntermediatePaths() async throws {
|
|
var path = try await self.fs.temporaryFilePath().underlying
|
|
for i in 0..<100 {
|
|
path.append("\(i)")
|
|
}
|
|
|
|
try await self.fs.createDirectory(
|
|
at: NIOFilePath(path),
|
|
withIntermediateDirectories: true,
|
|
permissions: nil
|
|
)
|
|
|
|
try await self.fs.withDirectoryHandle(atPath: NIOFilePath(path)) { dir in
|
|
let info = try await dir.info()
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testCreateDirectoryAtPathWhichExists() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .fileAlreadyExists)
|
|
}
|
|
}
|
|
|
|
func testCreateDirectoryAtPathWhereParentDoesNotExist() async throws {
|
|
let parent = try await self.fs.temporaryFilePath().underlying
|
|
let path = NIOFilePath(parent.appending("path"))
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .invalidArgument)
|
|
}
|
|
}
|
|
|
|
func testCreateDirectoryIsIdempotentWhenAlreadyExists() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
|
|
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
|
|
|
|
try await self.fs.withDirectoryHandle(atPath: path) { dir in
|
|
let info = try await dir.info()
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testCreateDirectoryThroughSymlinkToExistingDirectoryIsIdempotent() async throws {
|
|
let realDir = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createDirectory(at: realDir, withIntermediateDirectories: false)
|
|
|
|
let linkPath = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: linkPath, withDestination: realDir)
|
|
|
|
try await self.fs.createDirectory(at: linkPath, withIntermediateDirectories: false)
|
|
|
|
try await self.fs.withDirectoryHandle(atPath: linkPath) { dir in
|
|
let info = try await dir.info()
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testCurrentWorkingDirectory() async throws {
|
|
let directory = try await self.fs.currentWorkingDirectory
|
|
XCTAssert(!directory.underlying.isEmpty)
|
|
XCTAssert(directory.underlying.isAbsolute)
|
|
}
|
|
|
|
func testTemporaryDirectory() async throws {
|
|
let directory = try await self.fs.temporaryDirectory
|
|
XCTAssert(!directory.underlying.isEmpty)
|
|
XCTAssert(directory.underlying.isAbsolute)
|
|
}
|
|
|
|
func testHomeDirectory() async throws {
|
|
let directory = try await self.fs.homeDirectory
|
|
XCTAssert(!directory.underlying.isEmpty)
|
|
XCTAssert(directory.underlying.isAbsolute)
|
|
let info = try await self.fs.info(forFileAt: directory, infoAboutSymbolicLink: false)
|
|
XCTAssertEqual(info?.type, .directory)
|
|
}
|
|
|
|
func testHomeDirectoryFromEnvironment() async throws {
|
|
// Should return a value when HOME is set (which it typically is)
|
|
if let path = Libc.homeDirectoryFromEnvironment() {
|
|
XCTAssert(!path.isEmpty)
|
|
XCTAssert(path.isAbsolute)
|
|
|
|
// Verify it matches the high-level API
|
|
let fsHome = try await self.fs.homeDirectory
|
|
XCTAssertEqual(path, fsHome.underlying)
|
|
} else {
|
|
// If it returns nil, then HOME check should fail
|
|
let home = getenv("HOME")
|
|
XCTAssertTrue(home == nil || home!.pointee == 0, "Expected HOME to be unset or empty")
|
|
|
|
#if os(Windows)
|
|
let profile = getenv("USERPROFILE")
|
|
XCTAssertTrue(profile == nil || profile!.pointee == 0, "Expected USERPROFILE to be unset or empty")
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android)
|
|
func testHomeDirectoryFromPasswd() throws {
|
|
#if targetEnvironment(simulator)
|
|
// Simulators don't have a passwd database entry for the current user.
|
|
throw XCTSkip("getpwuid_r has no passwd entry in simulator environments")
|
|
#else
|
|
// Should always succeed on Unix-like systems for the current user
|
|
let result = Libc.homeDirectoryFromPasswd()
|
|
switch result {
|
|
case .success(let path):
|
|
XCTAssert(!path.isEmpty)
|
|
XCTAssert(path.isAbsolute)
|
|
case .failure(let errno):
|
|
XCTFail("Expected success, got error: \(errno)")
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
func testInfo() async throws {
|
|
let info = try await self.fs.info(forFileAt: .testDataReadme, infoAboutSymbolicLink: false)
|
|
XCTAssertEqual(info?.type, .regular)
|
|
XCTAssertGreaterThan(info?.size ?? -1, Int64(0))
|
|
}
|
|
|
|
func testInfoResolvingSymbolicLinks() async throws {
|
|
let info = try await self.fs.info(
|
|
forFileAt: .testDataReadmeSymlink,
|
|
infoAboutSymbolicLink: false
|
|
)
|
|
XCTAssertEqual(info?.type, .regular)
|
|
XCTAssertGreaterThan(info?.size ?? -1, Int64(0))
|
|
}
|
|
|
|
func testInfoWithoutResolvingSymbolicLinks() async throws {
|
|
let info = try await self.fs.info(
|
|
forFileAt: .testDataReadmeSymlink,
|
|
infoAboutSymbolicLink: true
|
|
)
|
|
XCTAssertEqual(info?.type, .symlink)
|
|
XCTAssertGreaterThan(info?.size ?? -1, Int64(0))
|
|
}
|
|
|
|
func testCreateSymbolicLink() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
let destination = NIOFilePath.testDataReadme
|
|
|
|
try await self.fs.createSymbolicLink(at: path, withDestination: destination)
|
|
let info = try await self.fs.info(forFileAt: destination, infoAboutSymbolicLink: true)
|
|
let infoViaLink = try await self.fs.info(forFileAt: path, infoAboutSymbolicLink: false)
|
|
XCTAssertEqual(info, infoViaLink)
|
|
}
|
|
|
|
func testDestinationOfSymbolicLink() async throws {
|
|
do {
|
|
// Relative symbolic link.
|
|
let destination = try await self.fs.destinationOfSymbolicLink(
|
|
at: .testDataReadmeSymlink
|
|
)
|
|
XCTAssertEqual(destination, "README.md")
|
|
}
|
|
|
|
do {
|
|
// Absolute symbolic link.
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: path, withDestination: .testDataReadme)
|
|
let destination = try await self.fs.destinationOfSymbolicLink(at: path)
|
|
XCTAssertEqual(destination, .testDataReadme)
|
|
}
|
|
}
|
|
|
|
func testCopySingleFile() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.copyItem(at: .testDataReadme, to: path)
|
|
|
|
try await self.fs.withFileHandle(forReadingAt: path) { copy in
|
|
try await self.fs.withFileHandle(forReadingAt: .testDataReadme) { original in
|
|
let originalContents = try await original.readToEnd(
|
|
maximumSizeAllowed: .bytes(1024 * 1024)
|
|
)
|
|
let copyContents = try await copy.readToEnd(maximumSizeAllowed: .bytes(1024 * 1024))
|
|
XCTAssertEqual(originalContents, copyContents)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCopyLargeFile() async throws {
|
|
let sourcePath = try await self.fs.temporaryFilePath()
|
|
let destPath = try await self.fs.temporaryFilePath()
|
|
self.addTeardownBlock { [fs] in
|
|
_ = try? await fs.removeItem(at: sourcePath, strategy: .platformDefault)
|
|
_ = try? await fs.removeItem(at: destPath, strategy: .platformDefault)
|
|
}
|
|
|
|
let sourceInfo = try await self.fs.withFileHandle(
|
|
forWritingAt: sourcePath,
|
|
options: .newFile(replaceExisting: false)
|
|
) { file in
|
|
// On Linux we use sendfile to copy which has a limit of 2GB; write at least that much
|
|
// to much sure we handle files above that size correctly.
|
|
var bytesToWrite: Int64 = 3 * 1024 * 1024 * 1024
|
|
var offset: Int64 = 0
|
|
// Write a blob a handful of times to avoid consuming too much memory in one go.
|
|
let blob = [UInt8](repeating: 0, count: 1024 * 1024 * 32) // 32MB
|
|
while bytesToWrite > 0 {
|
|
try await file.write(contentsOf: blob, toAbsoluteOffset: offset)
|
|
offset += Int64(blob.count)
|
|
bytesToWrite -= Int64(blob.count)
|
|
}
|
|
|
|
return try await file.info()
|
|
}
|
|
|
|
try await self.fs.copyItem(at: sourcePath, to: destPath)
|
|
let destInfo = try await self.fs.info(forFileAt: destPath)
|
|
XCTAssertEqual(destInfo?.size, sourceInfo.size)
|
|
}
|
|
|
|
func testCopySingleFileCopiesAttributesAndPermissions() async throws {
|
|
let original = try await self.fs.temporaryFilePath()
|
|
let copy = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: original,
|
|
options: .newFile(replaceExisting: false, permissions: .ownerReadWrite)
|
|
) { file1 in
|
|
do {
|
|
try await file1.updateValueForAttribute([0, 1, 2, 3], attribute: "foo")
|
|
} catch let error as FileSystemError where error.code == .unsupported {
|
|
// Extended attributes are not always supported; swallow the error if we hit it.
|
|
}
|
|
}
|
|
|
|
try await self.fs.copyItem(at: original, to: copy)
|
|
|
|
try await self.fs.withFileHandle(forReadingAt: copy) { file2 in
|
|
let info = try await file2.info()
|
|
XCTAssertEqual(info.permissions, [.ownerReadWrite])
|
|
|
|
do {
|
|
let value = try await file2.valueForAttribute("foo")
|
|
XCTAssertEqual(value, [0, 1, 2, 3])
|
|
} catch let error as FileSystemError where error.code == .unsupported {
|
|
// Extended attributes are not always supported; swallow the error if we hit it.
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCopySymlink() async throws {
|
|
let copy = try await self.fs.temporaryFilePath()
|
|
try await self.fs.copyItem(at: .testDataReadmeSymlink, to: copy)
|
|
|
|
let info = try await self.fs.info(forFileAt: copy, infoAboutSymbolicLink: true)
|
|
XCTAssertEqual(info?.type, .symlink)
|
|
|
|
let destination = try await self.fs.destinationOfSymbolicLink(at: copy)
|
|
XCTAssertEqual(destination, "README.md")
|
|
}
|
|
|
|
/// This is is not quite the same as sequential, different code paths are used.
|
|
/// Tests using this ensure use of the parallel paths (which are more complex) while keeping actual
|
|
/// parallelism to minimal levels to make debugging simpler.
|
|
private static let minimalParallel: CopyStrategy = try! .parallel(maxDescriptors: 2)
|
|
|
|
func testCopyEmptyDirectorySequential() async throws {
|
|
try await testCopyEmptyDirectory(.sequential)
|
|
}
|
|
|
|
func testCopyEmptyDirectoryParallelMinimal() async throws {
|
|
try await testCopyEmptyDirectory(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopyEmptyDirectoryParallelDefault() async throws {
|
|
try await testCopyEmptyDirectory(.platformDefault)
|
|
}
|
|
|
|
private func testCopyEmptyDirectory(
|
|
_ copyStrategy: CopyStrategy
|
|
) async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
|
|
|
|
let copy = try await self.fs.temporaryFilePath()
|
|
try await self.fs.copyItem(at: path, to: copy, strategy: copyStrategy)
|
|
|
|
try await self.checkDirectoriesMatch(path, copy)
|
|
}
|
|
|
|
func testCopyDirectoryToExistingDestinationSequential() async throws {
|
|
try await self.testCopyDirectoryToExistingDestination(.sequential)
|
|
}
|
|
|
|
func testCopyDirectoryToExistingDestinationParallelMinimal() async throws {
|
|
try await self.testCopyDirectoryToExistingDestination(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopyDirectoryToExistingDestinationParallelDefault() async throws {
|
|
try await self.testCopyDirectoryToExistingDestination(.platformDefault)
|
|
}
|
|
|
|
private func testCopyDirectoryToExistingDestination(
|
|
_ strategy: CopyStrategy
|
|
) async throws {
|
|
let path1 = try await self.fs.temporaryFilePath()
|
|
let path2 = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createDirectory(at: path1, withIntermediateDirectories: false)
|
|
try await self.fs.createDirectory(at: path2, withIntermediateDirectories: false)
|
|
|
|
await XCTAssertThrowsErrorAsync {
|
|
try await self.fs.copyItem(at: path1, to: path2, strategy: strategy)
|
|
}
|
|
}
|
|
|
|
func testCopyOnGeneratedTreeStructureSequential() async throws {
|
|
try await testAnyCopyStrategyOnGeneratedTreeStructure(.sequential)
|
|
}
|
|
|
|
func testCopyOnGeneratedTreeStructureParallelMinimal() async throws {
|
|
try await testAnyCopyStrategyOnGeneratedTreeStructure(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopyOnGeneratedTreeStructureParallelDefault() async throws {
|
|
try await testAnyCopyStrategyOnGeneratedTreeStructure(.platformDefault)
|
|
}
|
|
|
|
private func testAnyCopyStrategyOnGeneratedTreeStructure(
|
|
_ copyStrategy: CopyStrategy,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
let items = try await self.generateDirectoryStructure(
|
|
root: path,
|
|
maxDepth: 4,
|
|
maxFilesPerDirectory: 10
|
|
)
|
|
|
|
let copy = try await self.fs.temporaryFilePath()
|
|
do {
|
|
try await self.fs.copyItem(at: path, to: copy, strategy: copyStrategy)
|
|
} catch {
|
|
// Leave breadcrumbs to make debugging easier.
|
|
XCTFail(
|
|
"Using \(copyStrategy) failed to copy \(items) from '\(path)' to '\(copy)'",
|
|
line: line
|
|
)
|
|
throw error
|
|
}
|
|
|
|
do {
|
|
try await self.checkDirectoriesMatch(path, copy)
|
|
} catch {
|
|
// Leave breadcrumbs to make debugging easier.
|
|
XCTFail(
|
|
"Using \(copyStrategy) failed to validate \(items) copied from '\(path)' to '\(copy)'",
|
|
line: line
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func testCopySelectivelySequential() async throws {
|
|
try await testCopySelectively(.sequential)
|
|
}
|
|
|
|
func testCopySelectivelyParallelMinimal() async throws {
|
|
try await testCopySelectively(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopySelectivelyParallelDefault() async throws {
|
|
try await testCopySelectively(.platformDefault)
|
|
}
|
|
|
|
private func testCopySelectively(
|
|
_ copyStrategy: CopyStrategy,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
|
|
// Only generate regular files. They'll be in the format 'file-N-regular'.
|
|
let _ = try await self.generateDirectoryStructure(
|
|
root: path,
|
|
maxDepth: 1,
|
|
maxFilesPerDirectory: 10,
|
|
directoryProbability: 0.0,
|
|
symbolicLinkProbability: 0.0
|
|
)
|
|
|
|
let copyPath = try await self.fs.temporaryFilePath()
|
|
try await self.fs.copyItem(at: path, to: copyPath, strategy: copyStrategy, replaceExisting: false) { _, error in
|
|
throw error
|
|
} shouldCopyItem: { source, destination in
|
|
// Copy the directory and 'file-1-regular'
|
|
(source.path == path) || (source.path.underlying.lastComponent!.string == "file-0-regular")
|
|
}
|
|
|
|
let paths = try await self.fs.withDirectoryHandle(atPath: copyPath) { dir in
|
|
try await dir.listContents().reduce(into: []) { $0.append($1) }
|
|
}
|
|
|
|
XCTAssertEqual(paths.count, 1)
|
|
XCTAssertEqual(paths.first?.name, "file-0-regular")
|
|
}
|
|
|
|
func testCopyCancelledPartWayThroughSequential() async throws {
|
|
try await testCopyCancelledPartWayThrough(.sequential)
|
|
}
|
|
|
|
func testCopyCancelledPartWayThroughParallelMinimal() async throws {
|
|
try await testCopyCancelledPartWayThrough(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopyCancelledPartWayThroughParallelDefault() async throws {
|
|
try await testCopyCancelledPartWayThrough(.platformDefault)
|
|
}
|
|
|
|
private func testCopyCancelledPartWayThrough(
|
|
_ copyStrategy: CopyStrategy,
|
|
line: UInt = #line
|
|
) async throws {
|
|
// Whitebox testing to cover specific scenarios
|
|
switch copyStrategy.wrapped {
|
|
case let .parallel(maxDescriptors):
|
|
// The use of nested directories here allows us to rely on deterministic ordering
|
|
// of the shouldCopy calls that are used to trigger the cancel. If we used files then directory
|
|
// listing is not deterministic in general and that could make tests unreliable.
|
|
// If maxDescriptors gets too high the resulting recursion might result in the test failing.
|
|
// At that stage the tests would need some rework, but it's not viewed as likely given that
|
|
// the maxDescriptors defaults should remain small.
|
|
|
|
// Each dir consumes two descriptors, so this source can cover all scenarios.
|
|
let depth = maxDescriptors + 1
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.generateDeterministicDirectoryStructure(
|
|
root: path,
|
|
structure: TestFileStructure.makeNestedDirs(depth) {
|
|
.init("dir-\(depth - $0)")!
|
|
}
|
|
)
|
|
|
|
// This covers cancelling before/at the point we reach the limit.
|
|
// If the maxDescriptors is sufficiently low we simply can't trigger
|
|
// inside that phase so don't try.
|
|
if maxDescriptors >= 4 {
|
|
try await testCopyCancelledPartWayThrough(copyStrategy, "early_complete", path) {
|
|
$0.name == "dir-0"
|
|
}
|
|
}
|
|
|
|
// This covers completing after we reach the steady state phase.
|
|
let triggerAt = "dir-\(maxDescriptors / 2 + 1)"
|
|
try await testCopyCancelledPartWayThrough(copyStrategy, "late_complete", path) {
|
|
$0.name == triggerAt
|
|
}
|
|
case .sequential:
|
|
// nothing much to whitebox test here
|
|
break
|
|
}
|
|
// Keep doing random ones as a sort of fuzzing, it previously highlighted some interesting cases
|
|
// that are now covered in the whitebox tests above
|
|
let randomPath = try await self.fs.temporaryFilePath()
|
|
let _ = try await self.generateDirectoryStructure(
|
|
root: randomPath,
|
|
// Ensure:
|
|
// - Parallelism is possible in directory scans.
|
|
// - There are sub directories underneath the point we trigger cancel
|
|
maxDepth: 4,
|
|
maxFilesPerDirectory: 10,
|
|
directoryProbability: 1.0,
|
|
symbolicLinkProbability: 0.0
|
|
)
|
|
try await testCopyCancelledPartWayThrough(
|
|
copyStrategy,
|
|
"randomly generated",
|
|
randomPath
|
|
) { source in
|
|
source.path != randomPath && NIOFilePath(source.path.underlying.removingLastComponent()) != randomPath
|
|
}
|
|
}
|
|
|
|
private func testCopyCancelledPartWayThrough(
|
|
_ copyStrategy: CopyStrategy,
|
|
_ description: String,
|
|
_ path: NIOFilePath,
|
|
triggerCancel: @escaping @Sendable (DirectoryEntry) -> Bool,
|
|
line: UInt = #line
|
|
) async throws {
|
|
|
|
let copyPath = try await self.fs.temporaryFilePath()
|
|
|
|
let requestedCancel = NIOLockedValueBox<Bool>(false)
|
|
let cancelRequested = expectation(description: "cancel requested")
|
|
|
|
let task = Task { [fs] in
|
|
try await fs.copyItem(at: path, to: copyPath, strategy: copyStrategy, replaceExisting: false) { _, error in
|
|
throw error
|
|
} shouldCopyItem: { source, destination in
|
|
// Abuse shouldCopy to trigger the cancellation after getting some way in.
|
|
if triggerCancel(source) {
|
|
let shouldSleep = requestedCancel.withLockedValue { requested in
|
|
if !requested {
|
|
requested = true
|
|
cancelRequested.fulfill()
|
|
return true
|
|
}
|
|
|
|
return requested
|
|
}
|
|
// Give the cancellation time to kick in, this should be more than plenty.
|
|
if shouldSleep {
|
|
do {
|
|
try await Task.sleep(nanoseconds: 3_000_000_000)
|
|
XCTFail("\(description) Should have been cancelled by now!")
|
|
} catch is CancellationError {
|
|
// This is fine - we got cancelled as desired, let the rest of the in flight
|
|
// logic wind down cleanly (we hope/assert)
|
|
} catch let error {
|
|
XCTFail("\(description) just expected a cancellation error not \(error)")
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
return "completed the copy"
|
|
}
|
|
|
|
// Timeout notes: locally this should be fine as a second but on a loaded
|
|
// CI instance this test can be flaky at that level.
|
|
// Since testing cancellation is deemed highly desirable this is retained at
|
|
// quite relaxed thresholds.
|
|
// If this threshold remains insufficient for stable use then this test is likely
|
|
// not tenable to run in CI
|
|
await fulfillment(of: [cancelRequested], timeout: 5)
|
|
task.cancel()
|
|
let result = await task.result
|
|
switch result {
|
|
case let .success(msg):
|
|
XCTFail("\(description) expected the cancellation to have happened : \(msg)")
|
|
|
|
case let .failure(err):
|
|
if err is CancellationError {
|
|
// success
|
|
} else {
|
|
XCTFail("\(description) expected CancellationError not \(err)")
|
|
}
|
|
}
|
|
// We can't assert anything about the state of the copy,
|
|
// it might happen to all finish in time depending on scheduling.
|
|
}
|
|
|
|
func testCopyNonExistentFileSequential() async throws {
|
|
try await testCopyNonExistentFile(.sequential)
|
|
}
|
|
|
|
func testCopyNonExistentFileParallelMinimal() async throws {
|
|
try await testCopyNonExistentFile(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopyNonExistentFileParallelDefault() async throws {
|
|
try await testCopyNonExistentFile(.platformDefault)
|
|
}
|
|
|
|
private func testCopyNonExistentFile(
|
|
_ copyStrategy: CopyStrategy,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.copyItem(at: source, to: destination, strategy: copyStrategy)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testCopyToExistingDestinationSequential() async throws {
|
|
try await testCopyToExistingDestination(.sequential)
|
|
}
|
|
|
|
func testCopyToExistingDestinationParallelMinimal() async throws {
|
|
try await testCopyToExistingDestination(Self.minimalParallel)
|
|
}
|
|
|
|
func testCopyToExistingDestinationParallelDefault() async throws {
|
|
try await testCopyToExistingDestination(.platformDefault)
|
|
}
|
|
|
|
private func testCopyToExistingDestination(
|
|
_ copyStrategy: CopyStrategy,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
// Touch both files.
|
|
for path in [source, destination] {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
}
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.copyItem(at: source, to: destination, strategy: copyStrategy)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .fileAlreadyExists)
|
|
}
|
|
}
|
|
|
|
func testRemoveSingleFile() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
|
|
let infoAfterCreation = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNotNil(infoAfterCreation)
|
|
|
|
let removed = try await self.fs.removeItem(at: path, strategy: .platformDefault)
|
|
XCTAssertEqual(removed, 1)
|
|
|
|
let infoAfterRemoval = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNil(infoAfterRemoval)
|
|
}
|
|
|
|
func testRemoveNonExistentFile() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
let info = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNil(info)
|
|
let removed = try await self.fs.removeItem(at: path, strategy: .platformDefault)
|
|
XCTAssertEqual(removed, 0)
|
|
}
|
|
|
|
func testRemoveDirectorySequentially() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
let created = try await self.generateDirectoryStructure(
|
|
root: path,
|
|
maxDepth: 3,
|
|
maxFilesPerDirectory: 10
|
|
)
|
|
|
|
let infoAfterCreation = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNotNil(infoAfterCreation)
|
|
|
|
// Removing a non-empty directory recursively should throw 'notEmpty'
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.removeItem(at: path, strategy: .sequential, recursively: false)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notEmpty)
|
|
}
|
|
|
|
let removed = try await self.fs.removeItem(at: path, strategy: .sequential)
|
|
XCTAssertEqual(created, removed)
|
|
|
|
let infoAfterRemoval = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNil(infoAfterRemoval)
|
|
}
|
|
|
|
func testRemoveDirectoryConcurrently() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
let created = try await self.generateDirectoryStructure(
|
|
root: path,
|
|
maxDepth: 3,
|
|
maxFilesPerDirectory: 10
|
|
)
|
|
|
|
let infoAfterCreation = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNotNil(infoAfterCreation)
|
|
|
|
// Removing a non-empty directory recursively should throw 'notEmpty'
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.removeItem(at: path, strategy: .parallel(maxDescriptors: 2), recursively: false)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notEmpty)
|
|
}
|
|
|
|
let removed = try await self.fs.removeItem(at: path, strategy: .parallel(maxDescriptors: 2))
|
|
XCTAssertEqual(created, removed)
|
|
|
|
let infoAfterRemoval = try await self.fs.info(forFileAt: path)
|
|
XCTAssertNil(infoAfterRemoval)
|
|
}
|
|
|
|
func testMoveRegularFile() async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
do {
|
|
let sourceInfo = try await self.fs.info(forFileAt: source)
|
|
XCTAssertNotNil(sourceInfo)
|
|
let destinationInfo = try await self.fs.info(forFileAt: destination)
|
|
XCTAssertNil(destinationInfo)
|
|
}
|
|
|
|
try await self.fs.moveItem(at: source, to: destination)
|
|
|
|
do {
|
|
let sourceInfo = try await self.fs.info(forFileAt: source)
|
|
XCTAssertNil(sourceInfo)
|
|
let destinationInfo = try await self.fs.info(forFileAt: destination)
|
|
XCTAssertNotNil(destinationInfo)
|
|
}
|
|
}
|
|
|
|
func testMoveSymbolicLink() async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.createSymbolicLink(at: source, withDestination: .testDataReadme)
|
|
|
|
do {
|
|
let sourceInfo = try await self.fs.info(forFileAt: source, infoAboutSymbolicLink: true)
|
|
XCTAssertNotNil(sourceInfo)
|
|
XCTAssertEqual(sourceInfo?.type, .symlink)
|
|
let destinationInfo = try await self.fs.info(forFileAt: destination)
|
|
XCTAssertNil(destinationInfo)
|
|
}
|
|
|
|
try await self.fs.moveItem(at: source, to: destination)
|
|
|
|
do {
|
|
let sourceInfo = try await self.fs.info(forFileAt: source)
|
|
XCTAssertNil(sourceInfo)
|
|
let destinationInfo = try await self.fs.info(
|
|
forFileAt: destination,
|
|
infoAboutSymbolicLink: true
|
|
)
|
|
XCTAssertNotNil(destinationInfo)
|
|
XCTAssertEqual(destinationInfo?.type, .symlink)
|
|
}
|
|
|
|
let linkDestination = try await self.fs.destinationOfSymbolicLink(at: destination)
|
|
XCTAssertEqual(linkDestination, .testDataReadme)
|
|
}
|
|
|
|
func testMoveDirectory() async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.createDirectory(at: source, withIntermediateDirectories: true)
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: NIOFilePath(source.underlying.appending("foo")),
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
|
|
do {
|
|
let sourceInfo = try await self.fs.info(forFileAt: source, infoAboutSymbolicLink: false)
|
|
XCTAssertNotNil(sourceInfo)
|
|
XCTAssertEqual(sourceInfo?.type, .directory)
|
|
let destinationInfo = try await self.fs.info(forFileAt: destination)
|
|
XCTAssertNil(destinationInfo)
|
|
}
|
|
|
|
try await self.fs.moveItem(at: source, to: destination)
|
|
|
|
let sourceInfo = try await self.fs.info(forFileAt: source)
|
|
XCTAssertNil(sourceInfo)
|
|
|
|
let items = try await self.fs.withDirectoryHandle(atPath: destination) { directory in
|
|
try await directory.listContents().reduce(into: []) { $0.append($1) }
|
|
}
|
|
|
|
XCTAssertEqual(items.count, 1)
|
|
XCTAssertEqual(items.first?.name, "foo")
|
|
}
|
|
|
|
func testMoveWhenSourceDoesNotExist() async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.moveItem(at: source, to: destination)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testMoveWhenDestinationAlreadyExists() async throws {
|
|
let source = try await self.fs.temporaryFilePath()
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
for path in [source, destination] {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
}
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.moveItem(at: source, to: destination)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .fileAlreadyExists)
|
|
}
|
|
}
|
|
|
|
func testReplaceFile(_ existingType: FileType?, with replacementType: FileType) async throws {
|
|
func makeRegularFile(at path: NIOFilePath) async throws {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
}
|
|
|
|
func makeSymbolicLink(at path: NIOFilePath) async throws {
|
|
try await self.fs.createSymbolicLink(at: path, withDestination: "/whatever")
|
|
}
|
|
|
|
func makeDirectory(at path: NIOFilePath) async throws {
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
|
|
}
|
|
|
|
func makeFile(ofType type: FileType, at path: NIOFilePath) async throws {
|
|
switch type {
|
|
case .regular:
|
|
try await makeRegularFile(at: path)
|
|
case .symlink:
|
|
try await makeSymbolicLink(at: path)
|
|
case .directory:
|
|
try await makeDirectory(at: path)
|
|
default:
|
|
XCTFail("Unexpected file type '\(type)'")
|
|
}
|
|
}
|
|
|
|
let existingPath = try await self.fs.temporaryFilePath()
|
|
let replacementPath = try await self.fs.temporaryFilePath()
|
|
if let existingType = existingType {
|
|
try await makeFile(ofType: existingType, at: existingPath)
|
|
}
|
|
try await makeFile(ofType: replacementType, at: replacementPath)
|
|
|
|
try await self.fs.replaceItem(at: existingPath, withItemAt: replacementPath)
|
|
|
|
let sourceInfo = try await self.fs.info(
|
|
forFileAt: existingPath,
|
|
infoAboutSymbolicLink: true
|
|
)
|
|
XCTAssertNotNil(sourceInfo)
|
|
XCTAssertEqual(sourceInfo?.type, replacementType)
|
|
|
|
let destinationInfo = try await self.fs.info(
|
|
forFileAt: replacementPath,
|
|
infoAboutSymbolicLink: true
|
|
)
|
|
XCTAssertNil(destinationInfo)
|
|
}
|
|
|
|
func testReplaceRegularFileWithRegularFile() async throws {
|
|
try await self.testReplaceFile(.regular, with: .regular)
|
|
}
|
|
|
|
func testReplaceRegularFileWithSymbolicLink() async throws {
|
|
try await self.testReplaceFile(.regular, with: .symlink)
|
|
}
|
|
|
|
func testReplaceRegularFileWithDirectory() async throws {
|
|
try await self.testReplaceFile(.regular, with: .directory)
|
|
}
|
|
|
|
func testReplaceSymbolicLinkWithRegularFile() async throws {
|
|
try await self.testReplaceFile(.symlink, with: .regular)
|
|
}
|
|
|
|
func testReplaceSymbolicLinkWithSymbolicLink() async throws {
|
|
try await self.testReplaceFile(.symlink, with: .symlink)
|
|
}
|
|
|
|
func testReplaceSymbolicLinkWithDirectory() async throws {
|
|
try await self.testReplaceFile(.symlink, with: .directory)
|
|
}
|
|
|
|
func testReplaceDirectoryWithRegularFile() async throws {
|
|
try await self.testReplaceFile(.directory, with: .regular)
|
|
}
|
|
|
|
func testReplaceDirectoryWithSymbolicLink() async throws {
|
|
try await self.testReplaceFile(.directory, with: .symlink)
|
|
}
|
|
|
|
func testReplaceDirectoryWithDirectory() async throws {
|
|
try await self.testReplaceFile(.directory, with: .directory)
|
|
}
|
|
|
|
func testReplaceNothingWithRegularFile() async throws {
|
|
try await self.testReplaceFile(.none, with: .regular)
|
|
}
|
|
|
|
func testReplaceNothingWithSymbolicLink() async throws {
|
|
try await self.testReplaceFile(.none, with: .symlink)
|
|
}
|
|
|
|
func testReplaceNothingWithDirectory() async throws {
|
|
try await self.testReplaceFile(.none, with: .directory)
|
|
}
|
|
|
|
func testReplaceWhenExistingFileDoesNotExist() async throws {
|
|
let existing = try await self.fs.temporaryFilePath()
|
|
let replacement = try await self.fs.temporaryFilePath()
|
|
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.replaceItem(at: replacement, withItemAt: existing)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .notFound)
|
|
}
|
|
}
|
|
|
|
func testWithFileSystem() async throws {
|
|
try await withFileSystem(numberOfThreads: 1) { fs in
|
|
let info = try await fs.info(forFileAt: .testDataReadme)
|
|
XCTAssertEqual(info?.type, .regular)
|
|
}
|
|
}
|
|
|
|
func testListContentsOfLargeDirectory() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
|
|
|
|
try await self.fs.withDirectoryHandle(atPath: path) { handle in
|
|
for i in 0..<1024 {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: NIOFilePath(path.underlying.appending("\(i)")),
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
}
|
|
|
|
let names = try await handle.listContents().reduce(into: []) {
|
|
$0.append($1.name)
|
|
}
|
|
|
|
let expected = (0..<1024).map {
|
|
String($0)
|
|
}
|
|
|
|
XCTAssertEqual(names.sorted(), expected.sorted())
|
|
}
|
|
}
|
|
|
|
func testWithTemporaryDirectory() async throws {
|
|
let fs = FileSystem.shared
|
|
|
|
let createdPath = try await fs.withTemporaryDirectory { directory, path in
|
|
let root = try await fs.temporaryDirectory
|
|
XCTAssert(path.underlying.starts(with: root.underlying))
|
|
return path
|
|
}
|
|
|
|
// Directory shouldn't exist any more.
|
|
let info = try await fs.info(forFileAt: createdPath)
|
|
XCTAssertNil(info)
|
|
}
|
|
|
|
func testWithTemporaryDirectoryPrefix() async throws {
|
|
let fs = FileSystem.shared
|
|
let prefix = try await fs.currentWorkingDirectory
|
|
|
|
let createdPath = try await fs.withTemporaryDirectory(prefix: prefix) { directory, path in
|
|
XCTAssert(path.underlying.starts(with: prefix.underlying))
|
|
return path
|
|
}
|
|
|
|
// Directory shouldn't exist any more.
|
|
let info = try await fs.info(forFileAt: createdPath)
|
|
XCTAssertNil(info)
|
|
}
|
|
|
|
func testWithTemporaryDirectoryRemovesContents() async throws {
|
|
let fs = FileSystem.shared
|
|
let createdPath = try await fs.withTemporaryDirectory { directory, path in
|
|
for name in ["foo", "bar", "baz"] {
|
|
try await directory.withFileHandle(forWritingAt: NIOFilePath(name)) { fh in
|
|
_ = try await fh.write(contentsOf: [1, 2, 3], toAbsoluteOffset: 0)
|
|
}
|
|
}
|
|
|
|
let entries = try await directory.listContents().reduce(into: []) { $0.append($1) }
|
|
let names = entries.map { $0.name }
|
|
XCTAssertEqual(names.sorted(), ["bar", "baz", "foo"])
|
|
|
|
return path
|
|
}
|
|
|
|
// Directory shouldn't exist any more.
|
|
let info = try await fs.info(forFileAt: createdPath)
|
|
XCTAssertNil(info)
|
|
}
|
|
}
|
|
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
extension FileSystemTests {
|
|
private func checkDirectoriesMatch(_ root1: NIOFilePath, _ root2: NIOFilePath) async throws {
|
|
func namesAndTypes(_ root: NIOFilePath) async throws -> [(String, FileType)] {
|
|
try await self.fs.withDirectoryHandle(atPath: root) { dir in
|
|
try await dir.listContents()
|
|
.reduce(into: []) { $0.append($1) }
|
|
.map { ($0.name, $0.type) }
|
|
.sorted(by: { lhs, rhs in lhs.0 < rhs.0 })
|
|
}
|
|
}
|
|
|
|
// Check if all named entries and types match.
|
|
let root1Entries = try await namesAndTypes(root1)
|
|
let root2Entries = try await namesAndTypes(root2)
|
|
XCTAssertEqual(root1Entries.map { $0.0 }, root2Entries.map { $0.0 })
|
|
XCTAssertEqual(root1Entries.map { $0.1 }, root2Entries.map { $0.1 })
|
|
|
|
// Now look at regular files: are they all the same?
|
|
for (path, type) in root1Entries where type == .regular {
|
|
try await self.checkRegularFilesMatch(
|
|
NIOFilePath(root1.underlying.appending(path)),
|
|
NIOFilePath(root2.underlying.appending(path))
|
|
)
|
|
}
|
|
|
|
// Are symbolic links all the same?
|
|
for (path, type) in root1Entries where type == .symlink {
|
|
try await self.checkSymbolicLinksMatch(
|
|
NIOFilePath(root1.underlying.appending(path)),
|
|
NIOFilePath(root2.underlying.appending(path))
|
|
)
|
|
}
|
|
|
|
// Finally, check directories.
|
|
for (path, type) in root1Entries where type == .directory {
|
|
try await self.checkDirectoriesMatch(
|
|
NIOFilePath(root1.underlying.appending(path)),
|
|
NIOFilePath(root2.underlying.appending(path))
|
|
)
|
|
}
|
|
}
|
|
|
|
private func checkRegularFilesMatch(_ path1: NIOFilePath, _ path2: NIOFilePath) async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: path1) { file1 in
|
|
try await self.fs.withFileHandle(forReadingAt: path2) { file2 in
|
|
let info1 = try await file1.info()
|
|
let info2 = try await file2.info()
|
|
XCTAssertEqual(info1.type, info2.type)
|
|
XCTAssertEqual(info1.size, info2.size)
|
|
XCTAssertEqual(info1.permissions, info2.permissions)
|
|
|
|
let file1Contents = try await file1.readToEnd(
|
|
maximumSizeAllowed: .bytes(1024 * 1024)
|
|
)
|
|
let file2Contents = try await file2.readToEnd(
|
|
maximumSizeAllowed: .bytes(1024 * 1024)
|
|
)
|
|
XCTAssertEqual(file1Contents, file2Contents)
|
|
|
|
do {
|
|
let file1Attributes = try await file1.attributeNames().sorted()
|
|
let file2Attributes = try await file2.attributeNames().sorted()
|
|
XCTAssertEqual(file1Attributes, file2Attributes)
|
|
|
|
for attribute in file1Attributes {
|
|
let value1 = try await file1.valueForAttribute(attribute)
|
|
let value2 = try await file2.valueForAttribute(attribute)
|
|
XCTAssertEqual(value1, value2)
|
|
}
|
|
} catch let error as FileSystemError where error.code == .unsupported {
|
|
// Extended attributes aren't supported on all platforms, so swallow any
|
|
// unavailable errors.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func checkSymbolicLinksMatch(_ path1: NIOFilePath, _ path2: NIOFilePath) async throws {
|
|
let destination1 = try await self.fs.destinationOfSymbolicLink(at: path1)
|
|
let destination2 = try await self.fs.destinationOfSymbolicLink(at: path2)
|
|
XCTAssertEqual(destination1, destination2)
|
|
}
|
|
|
|
/// Declare a directory structure in code to make with ``generateDeterministicDirectoryStructure``
|
|
fileprivate enum TestFileStructure {
|
|
case dir(_ name: FilePath.Component, _ contents: [TestFileStructure] = [])
|
|
case file(_ name: FilePath.Component)
|
|
// don't care about the destination yet
|
|
case symbolicLink(_ name: FilePath.Component)
|
|
|
|
static func makeNestedDirs(
|
|
_ depth: Int,
|
|
namer: (Int) -> FilePath.Component = { .init("dir-\($0)")! }
|
|
) -> [TestFileStructure] {
|
|
let name = namer(depth)
|
|
guard depth > 0 else {
|
|
return []
|
|
}
|
|
return [.dir(name, makeNestedDirs(depth - 1, namer: namer))]
|
|
}
|
|
|
|
static func makeManyFiles(
|
|
_ num: Int,
|
|
namer: (Int) -> FilePath.Component = { .init("file-\($0)")! }
|
|
) -> [TestFileStructure] {
|
|
(0..<num).map { .file(namer($0)) }
|
|
}
|
|
}
|
|
|
|
/// This generates a directory structure to cover specific scenarios easily
|
|
private func generateDeterministicDirectoryStructure(
|
|
root: NIOFilePath,
|
|
structure: [TestFileStructure]
|
|
) async throws {
|
|
// always make root
|
|
try await self.fs.createDirectory(
|
|
at: root,
|
|
withIntermediateDirectories: false,
|
|
permissions: nil
|
|
)
|
|
|
|
for item in structure {
|
|
switch item {
|
|
case let .dir(name, contents):
|
|
try await self.generateDeterministicDirectoryStructure(
|
|
root: NIOFilePath(root.underlying.appending(name)),
|
|
structure: contents
|
|
)
|
|
case let .file(name):
|
|
try await self.makeTestFile(NIOFilePath(root.underlying.appending(name)))
|
|
case let .symbolicLink(name):
|
|
try await self.fs.createSymbolicLink(
|
|
at: NIOFilePath(root.underlying.appending(name)),
|
|
withDestination: "nonexistent-destination"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func makeTestFile(
|
|
_ path: NIOFilePath,
|
|
tryAddAttribute: String? = .none
|
|
) async throws {
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: path,
|
|
options: .newFile(replaceExisting: false)
|
|
) { file in
|
|
if let tryAddAttribute {
|
|
let byteCount = (32...128).randomElement()!
|
|
do {
|
|
try await file.updateValueForAttribute(
|
|
Array(repeating: 0, count: byteCount),
|
|
attribute: tryAddAttribute
|
|
)
|
|
} catch let error as FileSystemError where error.code == .unsupported {
|
|
// Extended attributes are not supported on all platforms. Ignore
|
|
// errors if that's the case.
|
|
()
|
|
}
|
|
}
|
|
|
|
let byteCount = (512...1024).randomElement()!
|
|
try await file.write(
|
|
contentsOf: Array(repeating: 0, count: byteCount),
|
|
toAbsoluteOffset: 0
|
|
)
|
|
}
|
|
}
|
|
|
|
private func generateDirectoryStructure(
|
|
root: NIOFilePath,
|
|
maxDepth: Int,
|
|
maxFilesPerDirectory: Int,
|
|
directoryProbability: Double = 0.3,
|
|
symbolicLinkProbability: Double = 0.2
|
|
) async throws -> Int {
|
|
guard maxDepth > 0 else { return 0 }
|
|
|
|
func makeDirectory() -> Bool {
|
|
Double.random(in: 0..<1.0) <= directoryProbability
|
|
}
|
|
|
|
func makeSymbolicLink() -> Bool {
|
|
Double.random(in: 0..<1.0) <= symbolicLinkProbability
|
|
}
|
|
|
|
try await self.fs.createDirectory(
|
|
at: root,
|
|
withIntermediateDirectories: false,
|
|
permissions: nil
|
|
)
|
|
|
|
let itemsInThisDir = Int.random(in: 1...maxFilesPerDirectory)
|
|
var itemsCreated = 1
|
|
|
|
guard itemsInThisDir > 0 else {
|
|
return itemsCreated
|
|
}
|
|
|
|
let dirsToMake = try await self.fs.withDirectoryHandle(atPath: root) { dir in
|
|
var directoriesToMake = [FilePath]()
|
|
|
|
for i in 0..<itemsInThisDir {
|
|
if makeDirectory() {
|
|
directoriesToMake.append(root.underlying.appending("file-\(i)-directory"))
|
|
} else if makeSymbolicLink() {
|
|
let path = "file-\(i)-symlink"
|
|
try await self.fs.createSymbolicLink(
|
|
at: NIOFilePath(root.underlying.appending(path)),
|
|
withDestination: "nonexistent-destination"
|
|
)
|
|
itemsCreated += 1
|
|
} else {
|
|
let path = root.underlying.appending("file-\(i)-regular")
|
|
let attribute: String? = Bool.random() ? .some("attribute-{\(i)}") : .none
|
|
try await makeTestFile(NIOFilePath(path), tryAddAttribute: attribute)
|
|
itemsCreated += 1
|
|
}
|
|
}
|
|
|
|
return directoriesToMake
|
|
}
|
|
|
|
for path in dirsToMake {
|
|
itemsCreated += try await self.generateDirectoryStructure(
|
|
root: NIOFilePath(path),
|
|
maxDepth: maxDepth - 1,
|
|
maxFilesPerDirectory: maxFilesPerDirectory,
|
|
directoryProbability: directoryProbability,
|
|
symbolicLinkProbability: symbolicLinkProbability
|
|
)
|
|
}
|
|
|
|
return itemsCreated
|
|
}
|
|
|
|
func testCreateTemporaryDirectory() async throws {
|
|
let validTemporaryDirectoryTemplates = [
|
|
"ValidTemporaryDirectoryXXX", "ValidTemporaryDirectoryXXXX",
|
|
"ValidTemporaryDirectoryXXXXX", "ValidTemporaryDirectoryXXXXXX",
|
|
"XXXXX", "fooXbarXXXX", "foo.barXXXX",
|
|
]
|
|
|
|
for templateString in validTemporaryDirectoryTemplates {
|
|
let template = NIOFilePath(templateString)
|
|
|
|
// A random ending consisting only of 'X's could be generated, making the template
|
|
// equal to the generated file path, but the probability of this happening three
|
|
// times in a row is very low.
|
|
var temporaryDirectoryPath: NIOFilePath?
|
|
|
|
attempt: for _ in 1...3 {
|
|
do {
|
|
temporaryDirectoryPath = try await self.fs.createTemporaryDirectory(
|
|
template: template
|
|
)
|
|
break attempt
|
|
} catch let error as FileSystemError where error.code == .fileAlreadyExists {
|
|
// Try again.
|
|
continue
|
|
} catch {
|
|
XCTFail("Unexpected error creating temporary file")
|
|
return
|
|
}
|
|
}
|
|
|
|
guard let temporaryDirectoryPath = temporaryDirectoryPath else {
|
|
return XCTFail("Ran out of attempts to create a unique temporary directory")
|
|
}
|
|
|
|
// Clean up after ourselves.
|
|
self.addTeardownBlock { [fileSystem = self.fs] in
|
|
try await fileSystem.removeItem(at: temporaryDirectoryPath, strategy: .platformDefault)
|
|
}
|
|
|
|
guard let info = try await self.fs.info(forFileAt: temporaryDirectoryPath) else {
|
|
return XCTFail("The temporary directory could not be accessed.")
|
|
}
|
|
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
}
|
|
|
|
func testCreateTemporaryDirectoryInvalidTemplate() async throws {
|
|
let invalidTemporaryDirectoryTemplates = [
|
|
"", "InalidTemporaryDirectory", "InvalidTemporaryDirectoryX",
|
|
"InvalidTemporaryDirectoryXX", "Invalidxxx", "InvalidXxX",
|
|
]
|
|
for templateString in invalidTemporaryDirectoryTemplates {
|
|
let template = NIOFilePath(templateString)
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.createTemporaryDirectory(template: template)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .invalidArgument)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCreateTemporaryDirectoryWithIntermediatePaths() async throws {
|
|
let templateString = "\(#function)"
|
|
let templateRoot = FilePath(templateString)
|
|
var template = templateRoot
|
|
for i in 0..<10 {
|
|
template.append("\(i)")
|
|
}
|
|
template.append("pattern-XXXXXX")
|
|
let temporaryDirectoryPath = try await self.fs.createTemporaryDirectory(
|
|
template: NIOFilePath(template)
|
|
)
|
|
|
|
self.addTeardownBlock { [fileSystem = self.fs] in
|
|
try await fileSystem.removeItem(
|
|
at: NIOFilePath(templateRoot),
|
|
strategy: .platformDefault,
|
|
recursively: true
|
|
)
|
|
}
|
|
|
|
guard
|
|
let info = try await self.fs.info(
|
|
forFileAt: temporaryDirectoryPath,
|
|
infoAboutSymbolicLink: false
|
|
)
|
|
else {
|
|
XCTFail("The temporary directory could not be accessed.")
|
|
return
|
|
}
|
|
XCTAssertEqual(info.type, .directory)
|
|
XCTAssertGreaterThan(info.size, 0)
|
|
}
|
|
|
|
func testTemporaryDirectoryRespectsEnvironment() async throws {
|
|
if let envTmpDir = getenv("TMPDIR") {
|
|
let envTmpDirString = String(cString: envTmpDir)
|
|
let fsTempDirectory = try await fs.temporaryDirectory
|
|
XCTAssertEqual(fsTempDirectory.underlying, FilePath(envTmpDirString))
|
|
}
|
|
}
|
|
|
|
func testReadChunksRange() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: NIOFilePath(#filePath)) { handle in
|
|
let info = try await handle.info()
|
|
let size = info.size
|
|
let endIndex = size + 1
|
|
|
|
let ranges: [(Range<Int64>, Int)] = [
|
|
(0..<0, 0),
|
|
(0..<1, 1),
|
|
(0..<endIndex, Int(size)),
|
|
(1..<endIndex, Int(size - 1)),
|
|
(0..<endIndex + 1, Int(size)),
|
|
(1..<endIndex + 1, Int(size - 1)),
|
|
(0..<Int64.max, Int(size)),
|
|
]
|
|
|
|
for (range, expected) in ranges {
|
|
let byteCount = try await handle.readChunks(in: range).reduce(into: 0) {
|
|
$0 += $1.readableBytes
|
|
}
|
|
|
|
XCTAssertEqual(byteCount, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReadChunksClosedRange() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: NIOFilePath(#filePath)) { handle in
|
|
let info = try await handle.info()
|
|
let size = info.size
|
|
let endIndex = size + 1
|
|
|
|
let ranges: [(ClosedRange<Int64>, Int)] = [
|
|
(0...0, 1),
|
|
(0...1, 2),
|
|
// Clamped to file size.
|
|
(0...endIndex, Int(info.size)),
|
|
(0...(endIndex - 1), Int(info.size)),
|
|
// Short one byte.
|
|
(1...(endIndex - 1), Int(info.size - 1)),
|
|
(1...endIndex, Int(info.size - 1)),
|
|
(0...(Int64.max - 1), Int(size)),
|
|
]
|
|
|
|
for (range, expected) in ranges {
|
|
let byteCount = try await handle.readChunks(in: range).reduce(into: 0) {
|
|
$0 += $1.readableBytes
|
|
}
|
|
XCTAssertEqual(byteCount, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReadChunksPartialRangeUpTo() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: NIOFilePath(#filePath)) { handle in
|
|
let info = try await handle.info()
|
|
let size = info.size
|
|
let endIndex = size + 1
|
|
|
|
let ranges: [(PartialRangeUpTo<Int64>, Int)] = [
|
|
(..<0, 0),
|
|
(..<1, 1),
|
|
// Clamped to file size.
|
|
(..<endIndex, Int(info.size)),
|
|
// Exactly the file size.
|
|
(..<(endIndex - 1), Int(info.size)),
|
|
// One byte short.
|
|
(..<(endIndex - 2), Int(info.size - 1)),
|
|
(..<Int64.max, Int(size)),
|
|
]
|
|
|
|
for (range, expected) in ranges {
|
|
let byteCount = try await handle.readChunks(in: range).reduce(into: 0) {
|
|
$0 += $1.readableBytes
|
|
}
|
|
|
|
XCTAssertEqual(byteCount, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReadChunksPartialRangeThrough() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: NIOFilePath(#filePath)) { handle in
|
|
let info = try await handle.info()
|
|
let size = info.size
|
|
let endIndex = size + 1
|
|
|
|
let ranges: [(PartialRangeThrough<Int64>, Int)] = [
|
|
(...0, 1),
|
|
(...1, 2),
|
|
// Clamped to size.
|
|
(...endIndex, Int(info.size)),
|
|
(...(endIndex - 1), Int(info.size)),
|
|
// Exact size.
|
|
(...(endIndex - 2), Int(info.size)),
|
|
// One byte short
|
|
(...(endIndex - 3), Int(info.size - 1)),
|
|
(...(Int64.max - 1), Int(size)),
|
|
]
|
|
|
|
for (range, expected) in ranges {
|
|
let byteCount = try await handle.readChunks(in: range).reduce(into: 0) {
|
|
$0 += $1.readableBytes
|
|
}
|
|
|
|
XCTAssertEqual(byteCount, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReadChunksPartialRangeFrom() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: NIOFilePath(#filePath)) { handle in
|
|
let info = try await handle.info()
|
|
let size = info.size
|
|
let endIndex = size + 1
|
|
|
|
let ranges: [(PartialRangeFrom<Int64>, Int)] = [
|
|
(0..., Int(size)),
|
|
(1..., Int(size - 1)),
|
|
(endIndex..., 0),
|
|
((endIndex - 1)..., 0),
|
|
((endIndex - 2)..., 1),
|
|
]
|
|
|
|
for (range, expected) in ranges {
|
|
let byteCount = try await handle.readChunks(in: range).reduce(into: 0) {
|
|
$0 += $1.readableBytes
|
|
}
|
|
|
|
XCTAssertEqual(byteCount, expected, "\(range)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReadChunksUnboundedRange() async throws {
|
|
try await self.fs.withFileHandle(forReadingAt: NIOFilePath(#filePath)) { handle in
|
|
let info = try await handle.info()
|
|
let size = info.size
|
|
|
|
let byteCount = try await handle.readChunks(in: ...).reduce(into: 0) {
|
|
$0 += $1.readableBytes
|
|
}
|
|
|
|
XCTAssertEqual(byteCount, Int(size))
|
|
}
|
|
}
|
|
|
|
func testReadMoreThanByteBufferCapacity() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
// Set `maximumSizeAllowed` to 1 byte more than can be written to `ByteBuffer`.
|
|
try await fileHandle.readToEnd(
|
|
maximumSizeAllowed: .byteBufferCapacity + .bytes(1)
|
|
)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .resourceExhausted)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReadWithUnlimitedMaximumSizeAllowed() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in
|
|
await XCTAssertNoThrowAsync(
|
|
try await fileHandle.readToEnd(maximumSizeAllowed: .unlimited)
|
|
)
|
|
}
|
|
}
|
|
|
|
func testReadIntoArray() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in
|
|
_ = try await fileHandle.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let contents = try await Array(contentsOf: path, maximumSizeAllowed: .bytes(1024))
|
|
|
|
XCTAssertEqual(contents, [0, 1, 2])
|
|
}
|
|
|
|
func testReadIntoArraySlice() async throws {
|
|
let path = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.withFileHandle(forReadingAndWritingAt: path) { fileHandle in
|
|
_ = try await fileHandle.write(contentsOf: [0, 1, 2], toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let contents = try await ArraySlice(contentsOf: path, maximumSizeAllowed: .bytes(1024))
|
|
|
|
XCTAssertEqual(contents, [0, 1, 2])
|
|
}
|
|
|
|
func testCopyFileReplacingExistingFileSucceeds() async throws {
|
|
let sourceFileContent: [UInt8] = [1, 2, 3]
|
|
let source = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false, permissions: .ownerReadWrite)
|
|
) { handle in
|
|
try await handle.write(
|
|
contentsOf: sourceFileContent,
|
|
toAbsoluteOffset: 0
|
|
)
|
|
}
|
|
|
|
let existingFileContent: [UInt8] = [4, 5, 6]
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: destination,
|
|
options: .newFile(replaceExisting: false, permissions: .ownerReadWriteExecute)
|
|
) { handle in
|
|
try await handle.write(
|
|
contentsOf: existingFileContent,
|
|
toAbsoluteOffset: 0
|
|
)
|
|
}
|
|
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in
|
|
throw error
|
|
},
|
|
shouldCopyItem: { _, _ in
|
|
true
|
|
}
|
|
)
|
|
|
|
// verify destination now has source content and permissions
|
|
let destInfo = try await self.fs.info(forFileAt: destination)
|
|
XCTAssertEqual(destInfo?.permissions, .ownerReadWrite)
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceFileContent)
|
|
}
|
|
|
|
// verify source still exists with original content and permissions
|
|
let sourceInfo = try await self.fs.info(forFileAt: source)
|
|
XCTAssertEqual(sourceInfo?.permissions, .ownerReadWrite)
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceFileContent)
|
|
}
|
|
|
|
// verify no temp files are left (Linux only - Darwin uses COPYFILE_UNLINK)
|
|
#if canImport(Glibc) || canImport(Musl) || canImport(Bionic)
|
|
let destinationDirectory = NIOFilePath(destination.underlying.removingLastComponent())
|
|
let temporaryFiles = try await self.fs.withDirectoryHandle(atPath: destinationDirectory) { dir in
|
|
var temporaryFiles: [String] = []
|
|
for try await batch in dir.listContents().batched() {
|
|
for entry in batch where entry.name.hasPrefix(".tmp-") {
|
|
temporaryFiles.append(entry.name)
|
|
}
|
|
}
|
|
return temporaryFiles
|
|
}
|
|
XCTAssertTrue(temporaryFiles.isEmpty, "Found temp files: \(temporaryFiles)")
|
|
#endif
|
|
}
|
|
|
|
func testCopyFileToNonExistingDestinationSucceeds() async throws {
|
|
let sourceFileContent: [UInt8] = [7, 8, 9]
|
|
let source = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(
|
|
contentsOf: sourceFileContent,
|
|
toAbsoluteOffset: 0
|
|
)
|
|
}
|
|
|
|
// destination doesn't exist
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination now exists with expected content
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceFileContent)
|
|
}
|
|
|
|
// verify source still exists with original content
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceFileContent)
|
|
}
|
|
}
|
|
|
|
func testCopySymlinkReplacingExistingSymlinkSucceeds() async throws {
|
|
let sourceTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: sourceTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let sourceSymlink = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(
|
|
at: sourceSymlink,
|
|
withDestination: sourceTarget
|
|
)
|
|
|
|
let destinationTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: destinationTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let destinationSymlink = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(
|
|
at: destinationSymlink,
|
|
withDestination: destinationTarget
|
|
)
|
|
|
|
try await self.fs.copyItem(
|
|
at: sourceSymlink,
|
|
to: destinationSymlink,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination symlink now points to source's target
|
|
let destinationTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: destinationSymlink)
|
|
XCTAssertEqual(destinationTargetAfterCopy, sourceTarget)
|
|
|
|
// verify source symlink has not changed
|
|
let sourceTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: sourceSymlink)
|
|
XCTAssertEqual(sourceTargetAfterCopy, sourceTarget)
|
|
|
|
// verify no temp symlinks are left
|
|
let destinationDirectory = NIOFilePath(destinationSymlink.underlying.removingLastComponent())
|
|
let temporarySymlinks = try await self.fs.withDirectoryHandle(atPath: destinationDirectory) { dir in
|
|
var temporarySymlinks: [String] = []
|
|
for try await batch in dir.listContents().batched() {
|
|
for entry in batch where entry.name.hasPrefix(".tmp-link-") {
|
|
temporarySymlinks.append(entry.name)
|
|
}
|
|
}
|
|
return temporarySymlinks
|
|
}
|
|
XCTAssertTrue(temporarySymlinks.isEmpty, "Found temp symlinks: \(temporarySymlinks)")
|
|
}
|
|
|
|
func testCopySymlinkToNonExistingDestinationSucceeds() async throws {
|
|
let sourceTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: sourceTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let sourceSymlink = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(
|
|
at: sourceSymlink,
|
|
withDestination: sourceTarget
|
|
)
|
|
|
|
// destination symlink doesn't exist
|
|
let destinationSymlink = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.copyItem(
|
|
at: sourceSymlink,
|
|
to: destinationSymlink,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination symlink now exists and points to source's target
|
|
let destinationTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: destinationSymlink)
|
|
XCTAssertEqual(destinationTargetAfterCopy, sourceTarget)
|
|
|
|
// verify source symlink has not changed
|
|
let sourceTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: sourceSymlink)
|
|
XCTAssertEqual(sourceTargetAfterCopy, sourceTarget)
|
|
}
|
|
|
|
func testCopySymlinkToExistingDestinationFailsWithoutReplaceExisting() async throws {
|
|
let sourceTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: sourceTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let sourceSymlink = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: sourceSymlink, withDestination: sourceTarget)
|
|
|
|
let destinationTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: destinationTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let destinationSymlink = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: destinationSymlink, withDestination: destinationTarget)
|
|
|
|
// should fail with `fileAlreadyExists`
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.copyItem(
|
|
at: sourceSymlink,
|
|
to: destinationSymlink,
|
|
strategy: .platformDefault,
|
|
replaceExisting: false,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .fileAlreadyExists)
|
|
}
|
|
|
|
// verify destination symlink is unchanged
|
|
let destinationTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: destinationSymlink)
|
|
XCTAssertEqual(destinationTargetAfterCopy, destinationTarget)
|
|
|
|
// verify source symlink is unchanged
|
|
let sourceTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: sourceSymlink)
|
|
XCTAssertEqual(sourceTargetAfterCopy, sourceTarget)
|
|
}
|
|
|
|
func testCopyFileToExistingDestinationFailsWithoutReplaceExisting() async throws {
|
|
let sourceContent: [UInt8] = [1, 2, 3]
|
|
let source = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: sourceContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let destinationContent: [UInt8] = [4, 5, 6]
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: destination,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: destinationContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
// should fail with `fileAlreadyExists`
|
|
await XCTAssertThrowsFileSystemErrorAsync {
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: false,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
} onError: { error in
|
|
XCTAssertEqual(error.code, .fileAlreadyExists)
|
|
}
|
|
|
|
// verify the destination is unchanged
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), destinationContent)
|
|
}
|
|
|
|
// verify the source is unchanged
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
}
|
|
|
|
func testCopyFileReplacingExistingSymlinkSucceeds() async throws {
|
|
let sourceContent: [UInt8] = [10, 20, 30]
|
|
let source = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: sourceContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let symlinkTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: symlinkTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: destination, withDestination: symlinkTarget)
|
|
|
|
// verify destination is a symlink before copy
|
|
let infoBefore = try await self.fs.info(forFileAt: destination, infoAboutSymbolicLink: true)
|
|
XCTAssertEqual(infoBefore?.type, .symlink)
|
|
|
|
// overwrite symlink with regular file
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination is now a regular file
|
|
let infoAfter = try await self.fs.info(forFileAt: destination, infoAboutSymbolicLink: true)
|
|
XCTAssertEqual(infoAfter?.type, .regular)
|
|
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
|
|
// verify source is unchanged
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
}
|
|
|
|
func testCopySymlinkReplacingExistingFileSucceeds() async throws {
|
|
let destinationContent: [UInt8] = [40, 50, 60]
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: destination,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: destinationContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let symlinkTarget = try await self.fs.temporaryFilePath()
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: symlinkTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
let sourceSymlink = try await self.fs.temporaryFilePath()
|
|
try await self.fs.createSymbolicLink(at: sourceSymlink, withDestination: symlinkTarget)
|
|
|
|
// verify destination is a regular file before copy
|
|
let infoBefore = try await self.fs.info(forFileAt: destination, infoAboutSymbolicLink: true)
|
|
XCTAssertEqual(infoBefore?.type, .regular)
|
|
|
|
// overwrite regular file with symlink
|
|
try await self.fs.copyItem(
|
|
at: sourceSymlink,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination is now a symlink pointing to correct target
|
|
let infoAfter = try await self.fs.info(forFileAt: destination, infoAboutSymbolicLink: true)
|
|
XCTAssertEqual(infoAfter?.type, .symlink)
|
|
|
|
let target = try await self.fs.destinationOfSymbolicLink(at: destination)
|
|
XCTAssertEqual(target, symlinkTarget)
|
|
|
|
// verify source symlink is unchanged
|
|
let sourceTarget = try await self.fs.destinationOfSymbolicLink(at: sourceSymlink)
|
|
XCTAssertEqual(sourceTarget, symlinkTarget)
|
|
}
|
|
|
|
func testCopyDanglingSymlinkSucceeds() async throws {
|
|
let nonExistentTarget = try await self.fs.temporaryFilePath()
|
|
let sourceSymlink = try await self.fs.temporaryFilePath()
|
|
|
|
// dangling symbolic link
|
|
try await self.fs.createSymbolicLink(at: sourceSymlink, withDestination: nonExistentTarget)
|
|
let targetInfo = try await self.fs.info(forFileAt: nonExistentTarget)
|
|
XCTAssertNil(targetInfo)
|
|
|
|
let destinationSymlink = try await self.fs.temporaryFilePath()
|
|
|
|
try await self.fs.copyItem(
|
|
at: sourceSymlink,
|
|
to: destinationSymlink,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination symlink exists and points to same (non-existent) target
|
|
let destTarget = try await self.fs.destinationOfSymbolicLink(at: destinationSymlink)
|
|
XCTAssertEqual(destTarget, nonExistentTarget)
|
|
|
|
let destInfo = try await self.fs.info(forFileAt: destinationSymlink, infoAboutSymbolicLink: false)
|
|
XCTAssertNil(destInfo)
|
|
|
|
// verify source symlink is unchanged
|
|
let sourceTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: sourceSymlink)
|
|
XCTAssertEqual(sourceTargetAfterCopy, nonExistentTarget)
|
|
}
|
|
|
|
func testCopyFileReplacingExistingFileWithLargerSourceSucceeds() async throws {
|
|
let sourceContent: [UInt8] = Array(repeating: 0xAB, count: 1024)
|
|
let source = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: sourceContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let destinationContent: [UInt8] = [1, 2, 3]
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: destination,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: destinationContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination now has source content
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(2048))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
|
|
// verify source is unchanged
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(2048))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
}
|
|
|
|
func testCopyFileReplacingExistingFileWithSmallerSourceSucceeds() async throws {
|
|
let sourceContent: [UInt8] = [1, 2, 3]
|
|
let source = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: sourceContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
let destinationContent: [UInt8] = Array(repeating: 0xAB, count: 1024)
|
|
let destination = try await self.fs.temporaryFilePath()
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: destination,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: destinationContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
// verify destination now has source content
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(2048))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
|
|
// verify source is unchanged
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let contents = try await handle.readToEnd(maximumSizeAllowed: .bytes(2048))
|
|
XCTAssertEqual(Array(buffer: contents), sourceContent)
|
|
}
|
|
}
|
|
|
|
func testCopyFileWithRelativePathsSucceeds() async throws {
|
|
let source = NIOFilePath("source")
|
|
let destination = NIOFilePath("destination")
|
|
let sourceContent: [UInt8] = [1, 2, 3, 4, 5]
|
|
let destinationContent: [UInt8] = [9, 9, 9, 9, 9]
|
|
|
|
self.addTeardownBlock { [fs] in
|
|
_ = try? await fs.removeItem(at: source, strategy: .platformDefault)
|
|
_ = try? await fs.removeItem(at: destination, strategy: .platformDefault)
|
|
}
|
|
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: source,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: sourceContent, toAbsoluteOffset: 0)
|
|
}
|
|
_ = try await self.fs.withFileHandle(
|
|
forWritingAt: destination,
|
|
options: .newFile(replaceExisting: false)
|
|
) { handle in
|
|
try await handle.write(contentsOf: destinationContent, toAbsoluteOffset: 0)
|
|
}
|
|
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
try await self.fs.withFileHandle(forReadingAt: destination) { handle in
|
|
let destinationContentAfterCopy = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: destinationContentAfterCopy), sourceContent)
|
|
}
|
|
|
|
try await self.fs.withFileHandle(forReadingAt: source) { handle in
|
|
let sourceContentAfterCopy = try await handle.readToEnd(maximumSizeAllowed: .bytes(1024))
|
|
XCTAssertEqual(Array(buffer: sourceContentAfterCopy), sourceContent)
|
|
}
|
|
}
|
|
|
|
func testCopySymlinkWithRelativePathsSucceeds() async throws {
|
|
let sourceTarget = NIOFilePath("source-target")
|
|
let source = NIOFilePath("source-link")
|
|
let destinationTarget = NIOFilePath("destination-target")
|
|
let destination = NIOFilePath("destination-link")
|
|
|
|
self.addTeardownBlock { [fs] in
|
|
_ = try? await fs.removeItem(at: sourceTarget, strategy: .platformDefault)
|
|
_ = try? await fs.removeItem(at: destinationTarget, strategy: .platformDefault)
|
|
_ = try? await fs.removeItem(at: source, strategy: .platformDefault)
|
|
_ = try? await fs.removeItem(at: destination, strategy: .platformDefault)
|
|
}
|
|
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: sourceTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
try await self.fs.withFileHandle(
|
|
forWritingAt: destinationTarget,
|
|
options: .newFile(replaceExisting: false)
|
|
) { _ in }
|
|
try await self.fs.createSymbolicLink(at: source, withDestination: sourceTarget)
|
|
try await self.fs.createSymbolicLink(at: destination, withDestination: destinationTarget)
|
|
|
|
try await self.fs.copyItem(
|
|
at: source,
|
|
to: destination,
|
|
strategy: .platformDefault,
|
|
replaceExisting: true,
|
|
shouldProceedAfterError: { _, error in throw error },
|
|
shouldCopyItem: { _, _ in true }
|
|
)
|
|
|
|
let destinationTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: destination)
|
|
XCTAssertEqual(destinationTargetAfterCopy, sourceTarget)
|
|
|
|
let sourceTargetAfterCopy = try await self.fs.destinationOfSymbolicLink(at: source)
|
|
XCTAssertEqual(sourceTargetAfterCopy, sourceTarget)
|
|
}
|
|
}
|