Files
swift-nio/Tests/NIOFSIntegrationTests/FileSystemTests.swift
Stepan Ulyanin be8fdc13c0 Allow to copy files and symlinks while overwriting the destination (#3508)
Adds capability to NIOFS to copy regular files and symlink, allowing to
overwrite the destination.

### Motivation:

Per https://github.com/apple/swift-nio/issues/3403 and
https://github.com/apple/swift-nio/pull/3470 we want to add
`replaceExisting: bool` to `FileSystem.copyItem`.

### Modifications:

1. Adds `replaceExisting: bool` parameter to
`FileSystemProtocol.copyItem`.
2. Adds `replaceExisting: bool` parameter to `FileSystem.copyItem` and
implementation for regular files and symbolic links.
3. Adds tests.

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
2026-02-18 08:57:33 +00:00

2576 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() {
// 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
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)
}
}