diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift new file mode 100644 index 00000000..eefb5f4b --- /dev/null +++ b/Sources/System/FileControl.swift @@ -0,0 +1,24 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +// Strongly typed, Swifty interfaces to the most common and useful `fcntl` +// commands. + +extension FileDescriptor { + internal func _fcntl( + _ cmd: Command, _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + withUnsafeMutablePointer(to: &lock) { + system_fcntl(self.rawValue, cmd.rawValue, $0) + } + } + } +} diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift new file mode 100644 index 00000000..5a98e2a8 --- /dev/null +++ b/Sources/System/FileControlRaw.swift @@ -0,0 +1,63 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + + +#if !os(Windows) + +// - MARK: Commands + +// TODO: Make below API as part of broader `fcntl` support. +extension FileDescriptor { + /// Commands (and various constants) to pass to `fcntl`. + internal struct Command: RawRepresentable, Hashable { + internal let rawValue: CInt + + internal init(rawValue: CInt) { self.rawValue = rawValue } + + private init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Get open file description record locking information. + /// + /// The corresponding C constant is `F_GETLK`. + internal static var getOFDLock: Command { Command(_F_OFD_GETLK) } + + /// Set open file description record locking information. + /// + /// The corresponding C constant is `F_SETLK`. + internal static var setOFDLock: Command { Command(_F_OFD_SETLK) } + + /// Set open file description record locking information and wait until + /// the request can be completed. + /// + /// The corresponding C constant is `F_SETLKW`. + internal static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } + +#if !os(Linux) + /// Set open file description record locking information and wait until + /// the request can be completed, returning on timeout. + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + internal static var setOFDLockWaitTimout: Command { + Command(_F_OFD_SETLKWTIMEOUT) + } +#endif + + } +} +#endif diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift new file mode 100644 index 00000000..fa13efb3 --- /dev/null +++ b/Sources/System/FileLock.swift @@ -0,0 +1,387 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + +#if !os(Windows) +extension FileDescriptor { + /// Advisory record locks. + /// + /// The corresponding C type is `struct flock`. + @frozen + public struct FileLock: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FileLock + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileLock) { self.rawValue = rawValue } + } +} + +extension FileDescriptor.FileLock { + @_alwaysEmitIntoClient + public init() { self.init(rawValue: .init()) } + + /// The type of the locking operation. + /// + /// The corresponding C field is `l_type`. + @_alwaysEmitIntoClient + public var type: Kind { + get { Kind(rawValue: rawValue.l_type) } + set { rawValue.l_type = newValue.rawValue } + } + + /// The origin of the locked region. + /// + /// The corresponding C field is `l_whence`. + @_alwaysEmitIntoClient + public var origin: FileDescriptor.SeekOrigin { + get { FileDescriptor.SeekOrigin(rawValue: CInt(rawValue.l_whence)) } + set { rawValue.l_whence = Int16(newValue.rawValue) } + } + + /// The start offset (from the origin) of the locked region. + /// + /// The corresponding C field is `l_start`. + @_alwaysEmitIntoClient + public var start: Int64 { + get { Int64(rawValue.l_start) } + set { rawValue.l_start = CInterop.Offset(newValue) } + } + + /// The number of consecutive bytes to lock. + /// + /// The corresponding C field is `l_len`. + @_alwaysEmitIntoClient + public var length: Int64 { + get { Int64(rawValue.l_len) } + set { rawValue.l_len = CInterop.Offset(newValue) } + } + + /// The process ID of the lock holder (if applicable). + /// + /// The corresponding C field is `l_pid` + @_alwaysEmitIntoClient + public var pid: ProcessID { + get { ProcessID(rawValue: rawValue.l_pid) } + set { rawValue.l_pid = newValue.rawValue } + } +} + +// MARK: - Convenience for `struct flock` +extension FileDescriptor.FileLock { + // For OFD locks + internal init( + ofdType: Kind, + start: Int64, + length: Int64 + ) { + self.init() + self.type = ofdType + self.start = start + self.length = length + self.pid = ProcessID(rawValue: 0) + } +} + +extension FileDescriptor.FileLock { + /// The kind or type of a lock: read (aka "shared"), write (aka "exclusive"), or none + /// (aka "unlock"). + @frozen + public struct Kind: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.CShort + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.CShort) { self.rawValue = rawValue } + + /// Read lock (aka "shared") + /// + /// The corresponding C constant is `F_RDLCK`. + @_alwaysEmitIntoClient + public static var read: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_RDLCK)) + } + + /// Write lock (aka "exclusive") + /// + /// The corresponding C constant is `F_WRLCK`. + @_alwaysEmitIntoClient + public static var write: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_WRLCK)) + } + + /// No lock (aka "unlock"). + /// + /// The corresponding C constant is `F_UNLCK`. + @_alwaysEmitIntoClient + public static var none: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_UNLCK)) + } + + /// Shared (alias for `read`) + @_alwaysEmitIntoClient + public static var shared: Self { .read } + + /// Exclusive (alias for `write`) + @_alwaysEmitIntoClient + public static var exclusive: Self { .write } + + /// Unlock (alias for `none`) + @_alwaysEmitIntoClient + public static var unlock: Self { .none } + } +} + +extension FileDescriptor { + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock, that is if the syscall would throw + /// `.resourceTemporarilyUnavailable`(aka `EAGAIN`), this will return + /// `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// - Returns: `true` if the lock was aquired, `false` otherwise + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK`. + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _lock( + kind, + start: start, + length: len, + wait: false, + waitUntilTimeout: false, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil + } + + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock and `wait` is true, this will wait until + /// the lock can be set, otherwise returns `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - wait: if `true` will wait until the lock can be set + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKW`. + @discardableResult + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + wait: Bool, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _lock( + kind, + start: start, + length: len, + wait: wait, + waitUntilTimeout: false, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil + } + +#if !os(Linux) + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock and `waitUntilTimeout` is true, this will + /// wait until the lock can be set(or the operating system's timeout + /// expires), otherwise returns `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - waitUntilTimeout: if `true` will wait until the lock can be set or a timeout expires + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + waitUntilTimeout: Bool, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _lock( + kind, + start: start, + length: len, + wait: false, + waitUntilTimeout: waitUntilTimeout, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil + } +#endif + + /// Remove an open file description lock. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Calling `unlock()` is equivalent to passing `.none` as the lock kind to + /// `FileDescriptor.lock()`. + /// + /// - Parameters: + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or + /// `F_OFD_SETLKW` and a lock type of `F_UNLCK`. + @_alwaysEmitIntoClient + public func unlock( + byteRange: (some RangeExpression)? = Range?.none, + retryOnInterrupt: Bool = true + ) throws { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + guard try _lock( + .none, + start: start, + length: len, + wait: false, + waitUntilTimeout: false, + retryOnInterrupt: retryOnInterrupt + )?.get() != nil else { + // NOTE: Errno and syscall composition wasn't designed for the modern + // world. Releasing locks should always succeed and never be blocked + // by an existing lock held elsewhere. But there's always a chance + // that some effect (e.g. from NFS) causes `EGAIN` to be thrown for a + // different reason/purpose. Here, in the very unlikely situation + // that we somehow saw it, we convert the `nil` back to the error. + throw Errno.resourceTemporarilyUnavailable + } + } + + /// Internal lock entry point, returns `nil` if blocked by existing lock. + /// Both `wait` and `waitUntilTimeout` cannot both be true (passed as bools to avoid + /// spurious enum in the ABI). + @usableFromInline + internal func _lock( + _ kind: FileDescriptor.FileLock.Kind, + start: Int64, + length: Int64, + wait: Bool, + waitUntilTimeout: Bool, + retryOnInterrupt: Bool + ) -> Result<(), Errno>? { + precondition(!wait || !waitUntilTimeout) + let cmd: FileDescriptor.Command + if waitUntilTimeout { +#if os(Linux) + preconditionFailure("`waitUntilTimeout` unavailable on Linux") + cmd = .setOFDLock +#else + cmd = .setOFDLockWaitTimout +#endif + } else if wait { + cmd = .setOFDLockWait + } else { + cmd = .setOFDLock + } + var lock = FileDescriptor.FileLock( + ofdType: kind, start: start, length: length) + return _extractWouldBlock( + _fcntl(cmd, &lock, retryOnInterrupt: retryOnInterrupt)) + } +} + +#endif // !os(Windows) + diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 5e46bafe..d8428b4f 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -45,29 +45,50 @@ public enum CInterop { /// The C `char` type public typealias Char = CChar - #if os(Windows) + /// The C `short` type + public typealias CShort = Int16 + +#if os(Windows) /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On /// Windows, this is `UInt16` (a "wide" character). public typealias PlatformChar = UInt16 - #else +#else /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On /// Windows, this is `UInt16` (a "wide" character). public typealias PlatformChar = CInterop.Char - #endif +#endif - #if os(Windows) +#if os(Windows) /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on /// Windows it is UTF-16. Native strings may contain invalid Unicode, /// which will be handled by either error-correction or failing, depending /// on API. public typealias PlatformUnicodeEncoding = UTF16 - #else +#else /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on /// Windows it is UTF-16. Native strings may contain invalid Unicode, /// which will be handled by either error-correction or failing, depending /// on API. public typealias PlatformUnicodeEncoding = UTF8 - #endif +#endif + +#if !os(Windows) + /// The C `struct flock` type + public typealias FileLock = flock + + /// The C `pid_t` type + public typealias PID = pid_t + + /// The C `off_t` type. + /// + /// Note System generally standardizes on `Int64` where `off_t` + /// might otherwise appear. This typealias allows conversion code to be + /// emitted into client. + public typealias Offset = off_t +#endif + } + + diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 85f9f3de..02eca1aa 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -531,3 +531,59 @@ internal var _SEEK_HOLE: CInt { SEEK_HOLE } @_alwaysEmitIntoClient internal var _SEEK_DATA: CInt { SEEK_DATA } #endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _LOCK_SH: CInt { LOCK_SH } + +@_alwaysEmitIntoClient +internal var _LOCK_EX: CInt { LOCK_EX } + +@_alwaysEmitIntoClient +internal var _LOCK_NB: CInt { LOCK_NB } + +@_alwaysEmitIntoClient +internal var _LOCK_UN: CInt { LOCK_UN } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _F_OFD_SETLK: CInt { +#if os(Linux) + 37 // FIXME: F_OFD_SETLK +#else + 90 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_SETLKW: CInt { +#if os(Linux) + 38 // FIXME: F_OFD_SETLKW +#else + 91 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_GETLK: CInt { +#if os(Linux) + 36// FIXME: F_OFD_GETLK +#else + 92 // FIXME: use API when available +#endif +} + +#if !os(Linux) +@_alwaysEmitIntoClient +internal var _F_OFD_SETLKWTIMEOUT: CInt { + 93 // FIXME: use API when available +} +@_alwaysEmitIntoClient +internal var _F_OFD_GETLKPID: CInt { + 94 // FIXME: use API when available +} +#endif // !os(Linux) + +#endif // !os(Windows) + diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index c5c376c3..02754f59 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -133,3 +133,37 @@ internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { return ftruncate(fd, length) } #endif + +#if !os(Windows) +internal func system_flock(_ fd: Int32, _ operation: Int32) -> Int32 { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, operation) } +#endif + return flock(fd, operation) +} +#endif + +#if !os(Windows) +internal func system_fcntl(_ fd: Int32, _ cmd: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd) } + #endif + return fcntl(fd, cmd) + } + + internal func system_fcntl(_ fd: Int32, _ cmd: Int32, _ arg: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd, arg) } + #endif + return fcntl(fd, cmd, arg) + } + + internal func system_fcntl( + _ fd: Int32, _ cmd: Int32, _ arg: UnsafeMutableRawPointer + ) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd, arg) } + #endif + return fcntl(fd, cmd, arg) + } +#endif diff --git a/Sources/System/ProcessID.swift b/Sources/System/ProcessID.swift new file mode 100644 index 00000000..70049a30 --- /dev/null +++ b/Sources/System/ProcessID.swift @@ -0,0 +1,16 @@ + +#if !os(Windows) + +/// The process identifier (aka PID) used to uniquely identify an active process. +/// +/// The corresponding C type is `pid_t` +@frozen +public struct ProcessID: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.PID + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.PID) { self.rawValue = rawValue } +} + +#endif diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index 3a8df9ac..ce956fd1 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -43,6 +43,16 @@ internal func nothingOrErrno( valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } } +/// Promote `Errno.wouldBlock` / `Errno.resourceTemporarilyUnavailable` to `nil`. +internal func _extractWouldBlock( + _ value: Result +) -> Result? { + if case .failure(let err) = value, err == .wouldBlock { + return nil + } + return value +} + // Run a precondition for debug client builds internal func _debugPrecondition( _ condition: @autoclosure () -> Bool, @@ -128,6 +138,41 @@ extension MutableCollection where Element: Equatable { } } +/// Map byte offsets passed as a range expression to start+length, e.g. for +/// use in `struct flock`. +/// +/// Start can be negative, e.g. for use with `SEEK_CUR`. +/// +/// Convention: Half-open ranges or explicit `Int64.min` / `Int64.max` bounds +/// denote start or end. +/// +/// Passing `Int64.min` as the lower bound maps to a start offset of `0`, such +/// that `..<5` would map to `(start: 0, length: 5)`. +/// +/// Passing `Int64.max` as an upper bound maps to a length of `0` (i.e. rest +/// of file by convention), such that passing `5...` would map to `(start: 5, +/// length: 0)`. +/// +/// NOTE: This is a utility function and can return negative start offsets and +/// negative lengths E.g. `(-3)...` for user with `SEEK_CUR` and `...(-3)` +/// (TBD). It's up to the caller to check any additional invariants +/// +@_alwaysEmitIntoClient +internal func _mapByteRangeToByteOffsets( + _ byteRange: (some RangeExpression)? +) -> (start: Int64, length: Int64) { + let allInts = Int64.min..( to value: T?, _ body: (UnsafePointer?) throws -> R diff --git a/Tests/SystemTests/FileLockTest.swift b/Tests/SystemTests/FileLockTest.swift new file mode 100644 index 00000000..35e4ef75 --- /dev/null +++ b/Tests/SystemTests/FileLockTest.swift @@ -0,0 +1,81 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +func _range(_ r: some RangeExpression) -> Range { + r.relative(to: Int64.min.. A(write) -> FAIL: B(read/write) + XCTAssertTrue(try ofd_A.lock(.read)) + XCTAssertTrue(try ofd_A.lock(.write)) + XCTAssertTrue(try dup_A.lock(.write)) // redundant, but works + XCTAssertFalse(try ofd_B.lock(.read)) + XCTAssertFalse(try ofd_B.lock(.write)) + XCTAssertFalse(try dup_B.lock(.write)) + try dup_A.unlock() + + // A(read) -> B(read) -> FAIL: A/B(write) + // -> B(unlock) -> A(write) -> FAIL: B(read/write) + XCTAssertTrue(try dup_A.lock(.read)) + XCTAssertTrue(try ofd_B.lock(.read)) + XCTAssertFalse(try ofd_A.lock(.write)) + XCTAssertFalse(try dup_A.lock(.write)) + XCTAssertFalse(try ofd_B.lock(.write)) + XCTAssertFalse(try dup_B.lock(.write)) + try dup_B.unlock() + XCTAssertTrue(try ofd_A.lock(.write)) + XCTAssertFalse(try dup_B.lock(.read)) + XCTAssertFalse(try ofd_B.lock(.write)) + try dup_A.unlock() + + /// Byte ranges + + // A(read, ..<50) -> B(write, 50...) + // -> A(write, 10..<20) -> B(read, 40..<50) + // -> FAIL: B(read, 17..<18), A(read 60..<70) + // -> A(unlock, 11..<12) -> B(read, 11..<12) -> A(read, 11..<12) + // -> FAIL A/B(write, 11..<12) + XCTAssertTrue(try ofd_A.lock(.read, byteRange: ..<50)) + XCTAssertTrue(try ofd_B.lock(.write, byteRange: 50...)) + XCTAssertTrue(try ofd_A.lock(.write, byteRange: 10..<20)) + XCTAssertTrue(try ofd_B.lock(.read, byteRange: 40..<50)) + XCTAssertFalse(try ofd_B.lock(.read, byteRange: 17..<18)) + XCTAssertFalse(try ofd_A.lock(.read, byteRange: 60..<70)) + try dup_A.unlock(byteRange: 11..<12) + XCTAssertTrue(try ofd_B.lock(.read, byteRange: 11..<12)) + XCTAssertTrue(try ofd_A.lock(.read, byteRange: 11..<12)) + XCTAssertFalse(try ofd_B.lock(.write, byteRange: 11..<12)) + XCTAssertFalse(try ofd_A.lock(.write, byteRange: 11..<12)) + } + + func testFileLocksWaiting() { + // TODO: Test waiting, test waiting until timeouts + } +} + diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 8062aedc..05c2d220 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -28,7 +28,7 @@ final class FileOperationsTest: XCTestCase { let writeBuf = UnsafeRawBufferPointer(rawBuf) let writeBufAddr = writeBuf.baseAddress - let syscallTestCases: Array = [ + var syscallTestCases: Array = [ MockTestCase(name: "open", .interruptable, "a path", O_RDWR | O_APPEND) { retryOnInterrupt in _ = try FileDescriptor.open( diff --git a/Tests/SystemTests/InternalUnitTests.swift b/Tests/SystemTests/InternalUnitTests.swift new file mode 100644 index 00000000..2a08b6ed --- /dev/null +++ b/Tests/SystemTests/InternalUnitTests.swift @@ -0,0 +1,52 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +final class InternalUnitTests: XCTestCase { + + + func testFileOffsets() { + func test( + _ br: (some RangeExpression)?, + _ expected: (start: Int64, length: Int64) + ) { + let (start, len) = _mapByteRangeToByteOffsets(br) + XCTAssertEqual(start, expected.start) + XCTAssertEqual(len, expected.length) + } + + test(2..<5, (start: 2, length: 3)) + test(2...5, (start: 2, length: 4)) + + test(..<5, (start: 0, length: 5)) + test(...5, (start: 0, length: 6)) + test(5..., (start: 5, length: 0)) + + // E.g. for use in specifying n bytes behind SEEK_CUR + // + // FIXME: are these ok? the API is for absolute + // offsets... + test((-3)..., (start: -3, length: 0)) + test((-3)..<0, (start: -3, length: 3)) + + // Non-sensical: up to the caller + test(..<(-5), (start: 0, length: -5)) + + } + + + +} diff --git a/Tests/SystemTests/MockingTest.swift b/Tests/SystemTests/MockingTest.swift index 1f2c96da..0b22832a 100644 --- a/Tests/SystemTests/MockingTest.swift +++ b/Tests/SystemTests/MockingTest.swift @@ -47,4 +47,13 @@ final class MockingTest: XCTestCase { } XCTAssertFalse(mockingEnabled) } + + func testFCNTLMocking() { + MockingDriver.withMockingEnabled { driver in + XCTAssertTrue(mockingEnabled) + + // TODO: a handful of mock tests, especially those that have different number of parameters + } + + } } diff --git a/Tests/SystemTests/XCTestManifests.swift b/Tests/SystemTests/XCTestManifests.swift index de99bd81..b096f265 100644 --- a/Tests/SystemTests/XCTestManifests.swift +++ b/Tests/SystemTests/XCTestManifests.swift @@ -1,4 +1,4 @@ -#if !canImport(ObjectiveC) && swift(<5.5) +#if !canImport(ObjectiveC) import XCTest extension ErrnoTest { @@ -28,8 +28,11 @@ extension FileOperationsTest { static let __allTests__FileOperationsTest = [ ("testAdHocOpen", testAdHocOpen), ("testAdHocPipe", testAdHocPipe), + ("testFileLocks", testFileLocks), + ("testFileLocksWaiting", testFileLocksWaiting), ("testGithubIssues", testGithubIssues), ("testHelpers", testHelpers), + ("testResizeFile", testResizeFile), ("testSyscalls", testSyscalls), ] } @@ -86,11 +89,21 @@ extension FilePermissionsTest { ] } +extension InternalUnitTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__InternalUnitTests = [ + ("testFileOffsets", testFileOffsets), + ] +} + extension MockingTest { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` // to regenerate. static let __allTests__MockingTest = [ + ("testFCNTLMocking", testFCNTLMocking), ("testMocking", testMocking), ] } @@ -109,11 +122,36 @@ extension SystemStringTest { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__SystemStringTest = [ + ("test_FilePath_initWithArrayConversion", test_FilePath_initWithArrayConversion), + ("test_FilePath_initWithInoutConversion", test_FilePath_initWithInoutConversion), + ("test_FilePath_initWithStringConversion", test_FilePath_initWithStringConversion), + ("test_FilePathComponent_initWithArrayConversion", test_FilePathComponent_initWithArrayConversion), + ("test_FilePathComponent_initWithInoutConversion", test_FilePathComponent_initWithInoutConversion), + ("test_FilePathComponent_initWithStringConversion", test_FilePathComponent_initWithStringConversion), + ("test_FilePathRoot_initWithArrayConversion", test_FilePathRoot_initWithArrayConversion), + ("test_FilePathRoot_initWithInoutConversion", test_FilePathRoot_initWithInoutConversion), + ("test_FilePathRoot_initWithStringConversion", test_FilePathRoot_initWithStringConversion), + ("test_String_initWithArrayConversion", test_String_initWithArrayConversion), + ("test_String_initWithInoutConversion", test_String_initWithInoutConversion), + ("test_String_initWithStringConversion", test_String_initWithStringConversion), + ("test_String_validatingPlatformStringWithArrayConversion", test_String_validatingPlatformStringWithArrayConversion), + ("test_String_validatingPlatformStringWithInoutConversion", test_String_validatingPlatformStringWithInoutConversion), + ("test_String_validatingPlatformStringWithStringConversion", test_String_validatingPlatformStringWithStringConversion), ("testAdHoc", testAdHoc), ("testPlatformString", testPlatformString), ] } +extension UtilTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__UtilTests = [ + ("testCStringArray", testCStringArray), + ("testStackBuffer", testStackBuffer), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(ErrnoTest.__allTests__ErrnoTest), @@ -124,9 +162,11 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(FilePathSyntaxTest.__allTests__FilePathSyntaxTest), testCase(FilePathTest.__allTests__FilePathTest), testCase(FilePermissionsTest.__allTests__FilePermissionsTest), + testCase(InternalUnitTests.__allTests__InternalUnitTests), testCase(MockingTest.__allTests__MockingTest), testCase(SystemCharTest.__allTests__SystemCharTest), testCase(SystemStringTest.__allTests__SystemStringTest), + testCase(UtilTests.__allTests__UtilTests), ] } #endif