Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ca8c9b1e | |||
| 83318d3ff0 | |||
| 3c5f858a94 | |||
| 965f7dbbe1 | |||
| 536414bd44 | |||
| b7d5db0a55 | |||
| d0907b16c8 | |||
| 60eb4b4676 | |||
| 4ec9e9c2a2 | |||
| b2efdf4d65 | |||
| d9badfec9b | |||
| 4d0f29adf6 | |||
| f817bc5840 | |||
| 5755b70e1b | |||
| 83ac2af5d6 | |||
| a2f001a968 | |||
| 9390064581 | |||
| 7a45ee9e47 | |||
| 17a4b18333 | |||
| ad01101d3c | |||
| d1bbc94bdd | |||
| fb5a8dde6c | |||
| 9b71040f43 | |||
| 5e50ea48d6 | |||
| 92a804e9e4 | |||
| af4635d047 | |||
| addebef7f9 | |||
| 46022ef0d6 | |||
| 386dc5202c | |||
| 17166093c2 | |||
| c06b8ce64c | |||
| aab8a2302b | |||
| 5c8c83f914 | |||
| ef2d9f5a90 | |||
| 08ebb473f2 | |||
| d32a041159 | |||
| 1057a7fca6 | |||
| cee2be50e2 | |||
| 8161c3cf02 | |||
| bb0f301383 | |||
| 9da1347d12 | |||
| 4a512c6aa0 |
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
If needed, include code examples for the problem or steps to reproduce.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 0.7.0]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6, Simulator iPhone6 ...]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Version [e.g. 0.7.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution or feature you would like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -7,8 +7,8 @@ target 'SwiftAudio_Example' do
|
||||
target 'SwiftAudio_Tests' do
|
||||
inherit! :search_paths
|
||||
|
||||
pod 'Quick', '~> 1.3.0'
|
||||
pod 'Nimble' , '~> 7.3.0'
|
||||
pod 'Quick', '~> 2.0.0'
|
||||
pod 'Nimble' , '~> 8.0.0'
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
PODS:
|
||||
- Nimble (7.3.1)
|
||||
- Quick (1.3.2)
|
||||
- SwiftAudio (0.3.3)
|
||||
- Nimble (8.0.1)
|
||||
- Quick (2.0.0)
|
||||
- SwiftAudio (0.7.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Nimble (~> 7.3.0)
|
||||
- Quick (~> 1.3.0)
|
||||
- Nimble (~> 8.0.0)
|
||||
- Quick (~> 2.0.0)
|
||||
- SwiftAudio (from `../`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -18,10 +18,10 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
|
||||
Quick: 2623cb30d7a7f41ca62f684f679586558f483d46
|
||||
SwiftAudio: 2e712c3e04cf172d05639d7bb1516db7afd195da
|
||||
Nimble: 45f786ae66faa9a709624227fae502db55a8bdd0
|
||||
Quick: ce1276c7c27ba2da3cb2fd0cde053c3648b3b22d
|
||||
SwiftAudio: a7bb22e98fd48fd908f7ffca992166e0057ce8ea
|
||||
|
||||
PODFILE CHECKSUM: 8a75946cbc65d8d98176f80a88d8363a28d118ce
|
||||
PODFILE CHECKSUM: 08ea4075437f8ff3c4f84ed70827a8132461373f
|
||||
|
||||
COCOAPODS: 1.5.3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "SwiftAudio",
|
||||
"version": "0.3.3",
|
||||
"version": "0.7.2",
|
||||
"summary": "Easy audio streaming for iOS",
|
||||
"description": "SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.",
|
||||
"homepage": "https://github.com/jorgenhenrichsen/SwiftAudio",
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/jorgenhenrichsen/SwiftAudio.git",
|
||||
"tag": "0.3.3"
|
||||
"tag": "0.7.2"
|
||||
},
|
||||
"platforms": {
|
||||
"ios": "10.0"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
PODS:
|
||||
- Nimble (7.3.1)
|
||||
- Quick (1.3.2)
|
||||
- SwiftAudio (0.3.3)
|
||||
- Nimble (8.0.1)
|
||||
- Quick (2.0.0)
|
||||
- SwiftAudio (0.7.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Nimble (~> 7.3.0)
|
||||
- Quick (~> 1.3.0)
|
||||
- Nimble (~> 8.0.0)
|
||||
- Quick (~> 2.0.0)
|
||||
- SwiftAudio (from `../`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -18,10 +18,10 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
|
||||
Quick: 2623cb30d7a7f41ca62f684f679586558f483d46
|
||||
SwiftAudio: 2e712c3e04cf172d05639d7bb1516db7afd195da
|
||||
Nimble: 45f786ae66faa9a709624227fae502db55a8bdd0
|
||||
Quick: ce1276c7c27ba2da3cb2fd0cde053c3648b3b22d
|
||||
SwiftAudio: a7bb22e98fd48fd908f7ffca992166e0057ce8ea
|
||||
|
||||
PODFILE CHECKSUM: 8a75946cbc65d8d98176f80a88d8363a28d118ce
|
||||
PODFILE CHECKSUM: 08ea4075437f8ff3c4f84ed70827a8132461373f
|
||||
|
||||
COCOAPODS: 1.5.3
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[](https://cocoapods.org/pods/Nimble)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/Nimble)
|
||||
[](https://houndci.com)
|
||||
|
||||
Use Nimble to express the expected outcomes of Swift
|
||||
or Objective-C expressions. Inspired by
|
||||
@@ -306,8 +307,7 @@ In Nimble, it's easy to make expectations on values that are updated
|
||||
asynchronously. Just use `toEventually` or `toEventuallyNot`:
|
||||
|
||||
```swift
|
||||
// Swift 3.0 and later
|
||||
|
||||
// Swift
|
||||
DispatchQueue.main.async {
|
||||
ocean.add("dolphins")
|
||||
ocean.add("whales")
|
||||
@@ -316,17 +316,6 @@ expect(ocean).toEventually(contain("dolphins", "whales"))
|
||||
```
|
||||
|
||||
|
||||
```swift
|
||||
// Swift 2.3 and earlier
|
||||
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
ocean.add("dolphins")
|
||||
ocean.add("whales")
|
||||
}
|
||||
expect(ocean).toEventually(contain("dolphins", "whales"))
|
||||
```
|
||||
|
||||
|
||||
```objc
|
||||
// Objective-C
|
||||
|
||||
@@ -857,11 +846,7 @@ Notes:
|
||||
|
||||
## Swift Error Handling
|
||||
|
||||
If you're using Swift 2.0 or newer, you can use the `throwError` matcher to check if an error is thrown.
|
||||
|
||||
Note:
|
||||
The following code sample references the `Swift.Error` protocol.
|
||||
This is `Swift.ErrorProtocol` in versions of Swift prior to version 3.0.
|
||||
You can use the `throwError` matcher to check if an error is thrown.
|
||||
|
||||
```swift
|
||||
// Swift
|
||||
@@ -1277,7 +1262,7 @@ public func equal<T: Equatable>(expectedValue: T?) -> Predicate<T> {
|
||||
// Predicate { actual in ... }
|
||||
//
|
||||
// But shown with types here for clarity.
|
||||
return Predicate { (actual: Expression<T>) throws -> PredicateResult in
|
||||
return Predicate { (actualExpression: Expression<T>) throws -> PredicateResult in
|
||||
let msg = ExpectationMessage.expectedActualValueTo("equal <\(expectedValue)>")
|
||||
if let actualValue = try actualExpression.evaluate() {
|
||||
return PredicateResult(
|
||||
@@ -1673,11 +1658,11 @@ backported.
|
||||
The deprecating plan is a 3 major versions removal. Which is as follows:
|
||||
|
||||
1. Introduce new `Predicate` API, deprecation warning for old matcher APIs.
|
||||
(Nimble `v7.x.x`)
|
||||
(Nimble `v7.x.x` and `v8.x.x`)
|
||||
2. Introduce warnings on migration-path features (`.predicate`,
|
||||
`Predicate`-constructors with similar arguments to old API). (Nimble
|
||||
`v8.x.x`)
|
||||
3. Remove old API. (Nimble `v9.x.x`)
|
||||
`v9.x.x`)
|
||||
3. Remove old API. (Nimble `v10.x.x`)
|
||||
|
||||
|
||||
# Installing Nimble
|
||||
|
||||
@@ -13,5 +13,6 @@ public protocol AssertionHandler {
|
||||
///
|
||||
/// @see AssertionHandler
|
||||
public var NimbleAssertionHandler: AssertionHandler = { () -> AssertionHandler in
|
||||
// swiftlint:disable:previous identifier_name
|
||||
return isXCTestAvailable() ? NimbleXCTestHandler() : NimbleXCTestUnavailableHandler()
|
||||
}()
|
||||
|
||||
@@ -37,21 +37,48 @@ public class AssertionRecorder: AssertionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
extension NMBExceptionCapture {
|
||||
internal func tryBlockThrows(_ unsafeBlock: () throws -> Void) throws {
|
||||
var catchedError: Error?
|
||||
tryBlock {
|
||||
do {
|
||||
try unsafeBlock()
|
||||
} catch {
|
||||
catchedError = error
|
||||
}
|
||||
}
|
||||
if let error = catchedError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows you to temporarily replace the current Nimble assertion handler with
|
||||
/// the one provided for the scope of the closure.
|
||||
///
|
||||
/// Once the closure finishes, then the original Nimble assertion handler is restored.
|
||||
///
|
||||
/// @see AssertionHandler
|
||||
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler, closure: () throws -> Void) {
|
||||
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
|
||||
file: FileString = #file,
|
||||
line: UInt = #line,
|
||||
closure: () throws -> Void) {
|
||||
let environment = NimbleEnvironment.activeInstance
|
||||
let oldRecorder = environment.assertionHandler
|
||||
let capturer = NMBExceptionCapture(handler: nil, finally: ({
|
||||
environment.assertionHandler = oldRecorder
|
||||
}))
|
||||
environment.assertionHandler = tempAssertionHandler
|
||||
capturer.tryBlock {
|
||||
try! closure()
|
||||
|
||||
do {
|
||||
try capturer.tryBlockThrows {
|
||||
try closure()
|
||||
}
|
||||
} catch {
|
||||
let failureMessage = FailureMessage()
|
||||
failureMessage.stringValue = "unexpected error thrown: <\(error)>"
|
||||
let location = SourceLocation(file: file, line: line)
|
||||
tempAssertionHandler.assert(false, message: failureMessage, location: location)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
|
||||
private func from(objcPredicate: NMBPredicate) -> Predicate<NSObject> {
|
||||
return Predicate { actualExpression in
|
||||
@@ -15,6 +15,7 @@ internal struct ObjCMatcherWrapper: Matcher {
|
||||
|
||||
func matches(_ actualExpression: Expression<NSObject>, failureMessage: FailureMessage) -> Bool {
|
||||
return matcher.matches(
|
||||
// swiftlint:disable:next force_try
|
||||
({ try! actualExpression.evaluate() }),
|
||||
failureMessage: failureMessage,
|
||||
location: actualExpression.location)
|
||||
@@ -22,6 +23,7 @@ internal struct ObjCMatcherWrapper: Matcher {
|
||||
|
||||
func doesNotMatch(_ actualExpression: Expression<NSObject>, failureMessage: FailureMessage) -> Bool {
|
||||
return matcher.doesNotMatch(
|
||||
// swiftlint:disable:next force_try
|
||||
({ try! actualExpression.evaluate() }),
|
||||
failureMessage: failureMessage,
|
||||
location: actualExpression.location)
|
||||
@@ -30,11 +32,13 @@ internal struct ObjCMatcherWrapper: Matcher {
|
||||
|
||||
// Equivalent to Expectation, but for Nimble's Objective-C interface
|
||||
public class NMBExpectation: NSObject {
|
||||
// swiftlint:disable identifier_name
|
||||
internal let _actualBlock: () -> NSObject?
|
||||
internal var _negative: Bool
|
||||
internal let _file: FileString
|
||||
internal let _line: UInt
|
||||
internal var _timeout: TimeInterval = 1.0
|
||||
// swiftlint:enable identifier_name
|
||||
|
||||
@objc public init(actualBlock: @escaping () -> NSObject?, negative: Bool, file: FileString, line: UInt) {
|
||||
self._actualBlock = actualBlock
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
// swiftlint:disable line_length
|
||||
public typealias MatcherBlock = (_ actualExpression: Expression<NSObject>, _ failureMessage: FailureMessage) throws -> Bool
|
||||
@@ -8,8 +8,10 @@ public typealias FullMatcherBlock = (_ actualExpression: Expression<NSObject>, _
|
||||
// swiftlint:enable line_length
|
||||
|
||||
public class NMBObjCMatcher: NSObject, NMBMatcher {
|
||||
// swiftlint:disable identifier_name
|
||||
let _match: MatcherBlock
|
||||
let _doesNotMatch: MatcherBlock
|
||||
// swiftlint:enable identifier_name
|
||||
let canMatchNil: Bool
|
||||
|
||||
public init(canMatchNil: Bool, matcher: @escaping MatcherBlock, notMatcher: @escaping MatcherBlock) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
|
||||
/// "Global" state of Nimble is stored here. Only DSL functions should access / be aware of this
|
||||
/// class' existence
|
||||
internal class NimbleEnvironment {
|
||||
internal class NimbleEnvironment: NSObject {
|
||||
static var activeInstance: NimbleEnvironment {
|
||||
get {
|
||||
let env = Thread.current.threadDictionary["NimbleEnvironment"]
|
||||
@@ -20,6 +20,7 @@ internal class NimbleEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next todo
|
||||
// TODO: eventually migrate the global to this environment value
|
||||
var assertionHandler: AssertionHandler {
|
||||
get { return NimbleAssertionHandler }
|
||||
@@ -29,17 +30,14 @@ internal class NimbleEnvironment {
|
||||
var suppressTVOSAssertionWarning: Bool = false
|
||||
var awaiter: Awaiter
|
||||
|
||||
init() {
|
||||
let timeoutQueue: DispatchQueue
|
||||
if #available(OSX 10.10, *) {
|
||||
timeoutQueue = DispatchQueue.global(qos: .userInitiated)
|
||||
} else {
|
||||
timeoutQueue = DispatchQueue.global(priority: .high)
|
||||
}
|
||||
|
||||
override init() {
|
||||
let timeoutQueue = DispatchQueue.global(qos: .userInitiated)
|
||||
awaiter = Awaiter(
|
||||
waitLock: AssertionWaitLock(),
|
||||
asyncQueue: .main,
|
||||
timeoutQueue: timeoutQueue)
|
||||
timeoutQueue: timeoutQueue
|
||||
)
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class NimbleXCTestUnavailableHandler: AssertionHandler {
|
||||
#endif
|
||||
|
||||
func isXCTestAvailable() -> Bool {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
// XCTest is weakly linked and so may not be present
|
||||
return NSClassFromString("XCTestCase") != nil
|
||||
#else
|
||||
@@ -77,15 +77,14 @@ public func recordFailure(_ message: String, location: SourceLocation) {
|
||||
XCTFail("\(message)", file: location.file, line: location.line)
|
||||
#else
|
||||
if let testCase = CurrentTestCaseTracker.sharedInstance.currentTestCase {
|
||||
#if swift(>=4)
|
||||
let line = Int(location.line)
|
||||
#else
|
||||
let line = location.line
|
||||
#endif
|
||||
testCase.recordFailure(withDescription: message, inFile: location.file, atLine: line, expected: true)
|
||||
} else {
|
||||
let msg = "Attempted to report a test failure to XCTest while no test case was running. " +
|
||||
"The failure was:\n\"\(message)\"\nIt occurred at: \(location.file):\(location.line)"
|
||||
let msg = """
|
||||
Attempted to report a test failure to XCTest while no test case was running. The failure was:
|
||||
\"\(message)\"
|
||||
It occurred at: \(location.file):\(location.line)
|
||||
"""
|
||||
NSException(name: .internalInconsistencyException, reason: msg, userInfo: nil).raise()
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,7 @@ internal class NMBWait: NSObject {
|
||||
// About these kind of lines, `@objc` attributes are only required for Objective-C
|
||||
// support, so that should be conditional on Darwin platforms and normal Xcode builds
|
||||
// (non-SwiftPM builds).
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objc
|
||||
internal class func until(
|
||||
timeout: TimeInterval,
|
||||
@@ -87,13 +87,19 @@ internal class NMBWait: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objc(untilFile:line:action:)
|
||||
internal class func until(_ file: FileString = #file, line: UInt = #line, action: @escaping (() -> Void) -> Void) {
|
||||
internal class func until(
|
||||
_ file: FileString = #file,
|
||||
line: UInt = #line,
|
||||
action: @escaping (@escaping () -> Void) -> Void) {
|
||||
until(timeout: 1, file: file, line: line, action: action)
|
||||
}
|
||||
#else
|
||||
internal class func until(_ file: FileString = #file, line: UInt = #line, action: @escaping (() -> Void) -> Void) {
|
||||
internal class func until(
|
||||
_ file: FileString = #file,
|
||||
line: UInt = #line,
|
||||
action: @escaping (@escaping () -> Void) -> Void) {
|
||||
until(timeout: 1, file: file, line: line, action: action)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -43,12 +43,13 @@ internal func nimblePrecondition(
|
||||
line: UInt = #line) {
|
||||
let result = expr()
|
||||
if !result {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
let e = NSException(
|
||||
#if canImport(Darwin)
|
||||
let exception = NSException(
|
||||
name: NSExceptionName(name()),
|
||||
reason: message(),
|
||||
userInfo: nil)
|
||||
e.raise()
|
||||
userInfo: nil
|
||||
)
|
||||
exception.raise()
|
||||
#else
|
||||
preconditionFailure("\(name()) - \(message())", file: file, line: line)
|
||||
#endif
|
||||
@@ -56,9 +57,12 @@ internal func nimblePrecondition(
|
||||
}
|
||||
|
||||
internal func internalError(_ msg: String, file: FileString = #file, line: UInt = #line) -> Never {
|
||||
// swiftlint:disable line_length
|
||||
fatalError(
|
||||
"Nimble Bug Found: \(msg) at \(file):\(line).\n" +
|
||||
"Please file a bug to Nimble: https://github.com/Quick/Nimble/issues with the " +
|
||||
"code snippet that caused this error."
|
||||
"""
|
||||
Nimble Bug Found: \(msg) at \(file):\(line).
|
||||
Please file a bug to Nimble: https://github.com/Quick/Nimble/issues with the code snippet that caused this error.
|
||||
"""
|
||||
)
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public indirect enum ExpectationMessage {
|
||||
}
|
||||
|
||||
internal func visitLeafs(_ f: (ExpectationMessage) -> ExpectationMessage) -> ExpectationMessage {
|
||||
// swiftlint:disable:previous identifier_name
|
||||
switch self {
|
||||
case .fail, .expectedTo, .expectedActualValueTo, .expectedCustomValueTo:
|
||||
return f(self)
|
||||
@@ -90,6 +91,7 @@ public indirect enum ExpectationMessage {
|
||||
/// Replaces a primary expectation with one returned by f. Preserves all composite expectations
|
||||
/// that were built upon it (aka - all appended(message:) and appended(details:).
|
||||
public func replacedExpectation(_ f: @escaping (ExpectationMessage) -> ExpectationMessage) -> ExpectationMessage {
|
||||
// swiftlint:disable:previous identifier_name
|
||||
func walk(_ msg: ExpectationMessage) -> ExpectationMessage {
|
||||
switch msg {
|
||||
case .fail, .expectedTo, .expectedActualValueTo, .expectedCustomValueTo:
|
||||
@@ -124,6 +126,7 @@ public indirect enum ExpectationMessage {
|
||||
return visitLeafs(walk)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next todo
|
||||
// TODO: test & verify correct behavior
|
||||
internal func prepended(message: String) -> ExpectationMessage {
|
||||
return .prepends(message, self)
|
||||
@@ -183,32 +186,32 @@ public indirect enum ExpectationMessage {
|
||||
|
||||
extension FailureMessage {
|
||||
internal func toExpectationMessage() -> ExpectationMessage {
|
||||
let defaultMsg = FailureMessage()
|
||||
if expected != defaultMsg.expected || _stringValueOverride != nil {
|
||||
let defaultMessage = FailureMessage()
|
||||
if expected != defaultMessage.expected || _stringValueOverride != nil {
|
||||
return .fail(stringValue)
|
||||
}
|
||||
|
||||
var msg: ExpectationMessage = .fail(userDescription ?? "")
|
||||
var message: ExpectationMessage = .fail(userDescription ?? "")
|
||||
if actualValue != "" && actualValue != nil {
|
||||
msg = .expectedCustomValueTo(postfixMessage, actualValue ?? "")
|
||||
} else if postfixMessage != defaultMsg.postfixMessage {
|
||||
message = .expectedCustomValueTo(postfixMessage, actualValue ?? "")
|
||||
} else if postfixMessage != defaultMessage.postfixMessage {
|
||||
if actualValue == nil {
|
||||
msg = .expectedTo(postfixMessage)
|
||||
message = .expectedTo(postfixMessage)
|
||||
} else {
|
||||
msg = .expectedActualValueTo(postfixMessage)
|
||||
message = .expectedActualValueTo(postfixMessage)
|
||||
}
|
||||
}
|
||||
if postfixActual != defaultMsg.postfixActual {
|
||||
msg = .appends(msg, postfixActual)
|
||||
if postfixActual != defaultMessage.postfixActual {
|
||||
message = .appends(message, postfixActual)
|
||||
}
|
||||
if let m = extendedMessage {
|
||||
msg = .details(msg, m)
|
||||
if let extended = extendedMessage {
|
||||
message = .details(message, extended)
|
||||
}
|
||||
return msg
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
public class NMBExpectationMessage: NSObject {
|
||||
private let msg: ExpectationMessage
|
||||
|
||||
@@ -24,8 +24,10 @@ internal func memoizedClosure<T>(_ closure: @escaping () throws -> T) -> (Bool)
|
||||
/// This provides a common consumable API for matchers to utilize to allow
|
||||
/// Nimble to change internals to how the captured closure is managed.
|
||||
public struct Expression<T> {
|
||||
// swiftlint:disable identifier_name
|
||||
internal let _expression: (Bool) throws -> T?
|
||||
internal let _withoutCaching: Bool
|
||||
// swiftlint:enable identifier_name
|
||||
public let location: SourceLocation
|
||||
public let isClosure: Bool
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public class FailureMessage: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next identifier_name
|
||||
internal var _stringValueOverride: String?
|
||||
internal var hasOverriddenStringValue: Bool {
|
||||
return _stringValueOverride != nil
|
||||
|
||||
@@ -63,7 +63,7 @@ private func createPredicate<S>(_ elementMatcher: Predicate<S.Iterator.Element>)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func allPassMatcher(_ matcher: NMBMatcher) -> NMBPredicate {
|
||||
return NMBPredicate { actualExpression in
|
||||
@@ -103,6 +103,7 @@ extension NMBObjCMatcher {
|
||||
} else {
|
||||
let failureMessage = FailureMessage()
|
||||
let result = matcher.matches(
|
||||
// swiftlint:disable:next force_try
|
||||
({ try! expr.evaluate() }),
|
||||
failureMessage: failureMessage,
|
||||
location: expr.location
|
||||
|
||||
@@ -23,14 +23,18 @@ private func async<T>(style: ExpectationStyle, predicate: Predicate<T>, timeout:
|
||||
}
|
||||
switch result {
|
||||
case .completed: return lastPredicateResult!
|
||||
case .timedOut: return PredicateResult(status: .fail, message: lastPredicateResult!.message)
|
||||
case .timedOut:
|
||||
let message = lastPredicateResult?.message ?? .fail("timed out before returning a value")
|
||||
return PredicateResult(status: .fail, message: message)
|
||||
case let .errorThrown(error):
|
||||
return PredicateResult(status: .fail, message: .fail("unexpected error thrown: <\(error)>"))
|
||||
case let .raisedException(exception):
|
||||
return PredicateResult(status: .fail, message: .fail("unexpected exception raised: \(exception)"))
|
||||
case .blockedRunLoop:
|
||||
// swiftlint:disable:next line_length
|
||||
return PredicateResult(status: .fail, message: lastPredicateResult!.message.appended(message: " (timed out, but main thread was unresponsive)."))
|
||||
let message = lastPredicateResult?.message.appended(message: " (timed out, but main run loop was unresponsive).") ??
|
||||
.fail("main run loop was unresponsive")
|
||||
return PredicateResult(status: .fail, message: message)
|
||||
case .incomplete:
|
||||
internalError("Reached .incomplete state for \(fnName)(...).")
|
||||
}
|
||||
@@ -38,8 +42,10 @@ private func async<T>(style: ExpectationStyle, predicate: Predicate<T>, timeout:
|
||||
}
|
||||
|
||||
private let toEventuallyRequiresClosureError = FailureMessage(
|
||||
// swiftlint:disable:next line_length
|
||||
stringValue: "expect(...).toEventually(...) requires an explicit closure (eg - expect { ... }.toEventually(...) )\nSwift 1.2 @autoclosure behavior has changed in an incompatible way for Nimble to function"
|
||||
stringValue: """
|
||||
expect(...).toEventually(...) requires an explicit closure (eg - expect { ... }.toEventually(...) )
|
||||
Swift 1.2 @autoclosure behavior has changed in an incompatible way for Nimble to function
|
||||
"""
|
||||
)
|
||||
|
||||
extension Expectation {
|
||||
|
||||
@@ -29,7 +29,7 @@ public func beAKindOf<T>(_ expectedType: T.Type) -> Predicate<Any> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual value is an instance of the given class.
|
||||
/// @see beAnInstanceOf if you want to match against the exact class
|
||||
|
||||
@@ -33,7 +33,7 @@ public func beAnInstanceOf(_ expectedClass: AnyClass) -> Predicate<NSObject> {
|
||||
} else {
|
||||
actualString = "<nil>"
|
||||
}
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
let matches = instance != nil && instance!.isMember(of: expectedClass)
|
||||
#else
|
||||
let matches = instance != nil && type(of: instance!) == expectedClass
|
||||
@@ -45,7 +45,7 @@ public func beAnInstanceOf(_ expectedClass: AnyClass) -> Predicate<NSObject> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beAnInstanceOfMatcher(_ expected: AnyClass) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable:next identifier_name
|
||||
public let DefaultDelta = 0.0001
|
||||
|
||||
internal func isCloseTo(_ actualValue: NMBDoubleConvertible?,
|
||||
@@ -34,10 +35,12 @@ public func beCloseTo(_ expectedValue: NMBDoubleConvertible, within delta: Doubl
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
public class NMBObjCBeCloseToMatcher: NSObject, NMBMatcher {
|
||||
// swiftlint:disable identifier_name
|
||||
var _expected: NSNumber
|
||||
var _delta: CDouble
|
||||
// swiftlint:enable identifier_name
|
||||
init(expected: NSNumber, within: CDouble) {
|
||||
_expected = expected
|
||||
_delta = within
|
||||
@@ -110,14 +113,17 @@ public func beCloseTo(_ expectedValues: [Double], within delta: Double = Default
|
||||
|
||||
infix operator ≈ : ComparisonPrecedence
|
||||
|
||||
// swiftlint:disable:next identifier_name
|
||||
public func ≈(lhs: Expectation<[Double]>, rhs: [Double]) {
|
||||
lhs.to(beCloseTo(rhs))
|
||||
}
|
||||
|
||||
// swiftlint:disable:next identifier_name
|
||||
public func ≈(lhs: Expectation<NMBDoubleConvertible>, rhs: NMBDoubleConvertible) {
|
||||
lhs.to(beCloseTo(rhs))
|
||||
}
|
||||
|
||||
// swiftlint:disable:next identifier_name
|
||||
public func ≈(lhs: Expectation<NMBDoubleConvertible>, rhs: (expected: NMBDoubleConvertible, delta: Double)) {
|
||||
lhs.to(beCloseTo(rhs.expected, within: rhs.delta))
|
||||
}
|
||||
@@ -133,6 +139,7 @@ precedencegroup PlusMinusOperatorPrecedence {
|
||||
}
|
||||
|
||||
infix operator ± : PlusMinusOperatorPrecedence
|
||||
// swiftlint:disable:next identifier_name
|
||||
public func ±(lhs: NMBDoubleConvertible, rhs: Double) -> (expected: NMBDoubleConvertible, delta: Double) {
|
||||
return (expected: lhs, delta: rhs)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,36 @@ import Foundation
|
||||
/// means the are no items in that collection. For strings, it is an empty string.
|
||||
public func beEmpty<S: Sequence>() -> Predicate<S> {
|
||||
return Predicate.simple("be empty") { actualExpression in
|
||||
let actualSeq = try actualExpression.evaluate()
|
||||
if actualSeq == nil {
|
||||
guard let actual = try actualExpression.evaluate() else {
|
||||
return .fail
|
||||
}
|
||||
var generator = actualSeq!.makeIterator()
|
||||
var generator = actual.makeIterator()
|
||||
return PredicateStatus(bool: generator.next() == nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when a value is "empty". For collections, this
|
||||
/// means the are no items in that collection. For strings, it is an empty string.
|
||||
public func beEmpty<S: SetAlgebra>() -> Predicate<S> {
|
||||
return Predicate.simple("be empty") { actualExpression in
|
||||
guard let actual = try actualExpression.evaluate() else {
|
||||
return .fail
|
||||
}
|
||||
return PredicateStatus(bool: actual.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when a value is "empty". For collections, this
|
||||
/// means the are no items in that collection. For strings, it is an empty string.
|
||||
public func beEmpty<S: Sequence & SetAlgebra>() -> Predicate<S> {
|
||||
return Predicate.simple("be empty") { actualExpression in
|
||||
guard let actual = try actualExpression.evaluate() else {
|
||||
return .fail
|
||||
}
|
||||
return PredicateStatus(bool: actual.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when a value is "empty". For collections, this
|
||||
/// means the are no items in that collection. For strings, it is an empty string.
|
||||
public func beEmpty() -> Predicate<String> {
|
||||
@@ -61,7 +82,7 @@ public func beEmpty() -> Predicate<NMBCollection> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beEmptyMatcher() -> NMBPredicate {
|
||||
return NMBPredicate { actualExpression in
|
||||
|
||||
@@ -30,12 +30,12 @@ public func > (lhs: Expectation<NMBComparable>, rhs: NMBComparable?) {
|
||||
lhs.to(beGreaterThan(rhs))
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beGreaterThanMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func beGreaterThanMatcher(_ expected: NMBComparable?) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { $0 as? NMBComparable }
|
||||
return try beGreaterThan(expected).matches(expr, failureMessage: failureMessage)
|
||||
return try beGreaterThan(expected).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ public func >=<T: NMBComparable>(lhs: Expectation<T>, rhs: T) {
|
||||
lhs.to(beGreaterThanOrEqualTo(rhs))
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beGreaterThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func beGreaterThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { $0 as? NMBComparable }
|
||||
return try beGreaterThanOrEqualTo(expected).matches(expr, failureMessage: failureMessage)
|
||||
return try beGreaterThanOrEqualTo(expected).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import Foundation
|
||||
/// as the expected instance.
|
||||
public func beIdenticalTo(_ expected: Any?) -> Predicate<Any> {
|
||||
return Predicate.define { actualExpression in
|
||||
#if os(Linux)
|
||||
#if os(Linux) && !swift(>=4.1.50)
|
||||
let actual = try actualExpression.evaluate() as? AnyObject
|
||||
#else
|
||||
let actual = try actualExpression.evaluate() as AnyObject?
|
||||
#endif
|
||||
|
||||
let bool: Bool
|
||||
#if os(Linux)
|
||||
#if os(Linux) && !swift(>=4.1.50)
|
||||
bool = actual === (expected as? AnyObject) && actual !== nil
|
||||
#else
|
||||
bool = actual === (expected as AnyObject?) && actual !== nil
|
||||
@@ -41,12 +41,12 @@ public func be(_ expected: Any?) -> Predicate<Any> {
|
||||
return beIdenticalTo(expected)
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beIdenticalToMatcher(_ expected: NSObject?) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func beIdenticalToMatcher(_ expected: NSObject?) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let aExpr = actualExpression.cast { $0 as Any? }
|
||||
return try beIdenticalTo(expected).matches(aExpr, failureMessage: failureMessage)
|
||||
return try beIdenticalTo(expected).satisfies(aExpr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ public func < (lhs: Expectation<NMBComparable>, rhs: NMBComparable?) {
|
||||
lhs.to(beLessThan(rhs))
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beLessThanMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func beLessThanMatcher(_ expected: NMBComparable?) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { $0 as? NMBComparable }
|
||||
return try beLessThan(expected).matches(expr, failureMessage: failureMessage)
|
||||
return try beLessThan(expected).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ public func <=<T: NMBComparable>(lhs: Expectation<T>, rhs: T) {
|
||||
lhs.to(beLessThanOrEqualTo(rhs))
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beLessThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func beLessThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { $0 as? NMBComparable }
|
||||
return try beLessThanOrEqualTo(expected).matches(expr, failureMessage: failureMessage)
|
||||
return try beLessThanOrEqualTo(expected).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +100,6 @@ public func beTruthy<T: ExpressibleByBooleanLiteral & Equatable>() -> Predicate<
|
||||
return Predicate.simpleNilable("be truthy") { actualExpression in
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
if let actualValue = actualValue {
|
||||
// FIXME: This is a workaround to SR-2290.
|
||||
// See:
|
||||
// - https://bugs.swift.org/browse/SR-2290
|
||||
// - https://github.com/norio-nomura/Nimble/pull/5#issuecomment-237835873
|
||||
if let number = actualValue as? NSNumber {
|
||||
return PredicateStatus(bool: number.boolValue == true)
|
||||
}
|
||||
|
||||
return PredicateStatus(bool: actualValue == (true as T))
|
||||
}
|
||||
return PredicateStatus(bool: actualValue != nil)
|
||||
@@ -120,47 +112,42 @@ public func beFalsy<T: ExpressibleByBooleanLiteral & Equatable>() -> Predicate<T
|
||||
return Predicate.simpleNilable("be falsy") { actualExpression in
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
if let actualValue = actualValue {
|
||||
// FIXME: This is a workaround to SR-2290.
|
||||
// See:
|
||||
// - https://bugs.swift.org/browse/SR-2290
|
||||
// - https://github.com/norio-nomura/Nimble/pull/5#issuecomment-237835873
|
||||
if let number = actualValue as? NSNumber {
|
||||
return PredicateStatus(bool: number.boolValue == false)
|
||||
}
|
||||
|
||||
return PredicateStatus(bool: actualValue == (false as T))
|
||||
}
|
||||
return PredicateStatus(bool: actualValue == nil)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beTruthyMatcher() -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher { actualExpression, failureMessage in
|
||||
@objc public class func beTruthyMatcher() -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
|
||||
return try beTruthy().matches(expr, failureMessage: failureMessage)
|
||||
return try beTruthy().satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
|
||||
@objc public class func beFalsyMatcher() -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher { actualExpression, failureMessage in
|
||||
@objc public class func beFalsyMatcher() -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
|
||||
return try beFalsy().matches(expr, failureMessage: failureMessage)
|
||||
return try beFalsy().satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
|
||||
@objc public class func beTrueMatcher() -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher { actualExpression, failureMessage in
|
||||
@objc public class func beTrueMatcher() -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
|
||||
return try beTrue().matches(expr, failureMessage: failureMessage)
|
||||
return try beTrue().satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
|
||||
@objc public class func beFalseMatcher() -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
|
||||
return try beFalse().matches(expr, failureMessage: failureMessage)
|
||||
@objc public class func beFalseMatcher() -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let expr = actualExpression.cast { value -> Bool? in
|
||||
guard let value = value else { return nil }
|
||||
return (value as? NSNumber)?.boolValue ?? false
|
||||
}
|
||||
return try beFalse().satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ public func beNil<T>() -> Predicate<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beNilMatcher() -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher { actualExpression, failureMessage in
|
||||
return try beNil().matches(actualExpression, failureMessage: failureMessage)
|
||||
@objc public class func beNilMatcher() -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
return try beNil().satisfies(actualExpression).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ public func beVoid() -> Predicate<()> {
|
||||
}
|
||||
}
|
||||
|
||||
public func == (lhs: Expectation<()>, rhs: ()) {
|
||||
lhs.to(beVoid())
|
||||
}
|
||||
extension Expectation where T == () {
|
||||
public static func == (lhs: Expectation<()>, rhs: ()) {
|
||||
lhs.to(beVoid())
|
||||
}
|
||||
|
||||
public func != (lhs: Expectation<()>, rhs: ()) {
|
||||
lhs.toNot(beVoid())
|
||||
public static func != (lhs: Expectation<()>, rhs: ()) {
|
||||
lhs.toNot(beVoid())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,24 +35,24 @@ public func beginWith(_ startingElement: Any) -> Predicate<NMBOrderedCollection>
|
||||
public func beginWith(_ startingSubstring: String) -> Predicate<String> {
|
||||
return Predicate.simple("begin with <\(startingSubstring)>") { actualExpression in
|
||||
if let actual = try actualExpression.evaluate() {
|
||||
let range = actual.range(of: startingSubstring)
|
||||
return PredicateStatus(bool: range != nil && range!.lowerBound == actual.startIndex)
|
||||
return PredicateStatus(bool: actual.hasPrefix(startingSubstring))
|
||||
}
|
||||
return .fail
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func beginWithMatcher(_ expected: Any) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func beginWithMatcher(_ expected: Any) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let actual = try actualExpression.evaluate()
|
||||
if (actual as? String) != nil {
|
||||
if actual is String {
|
||||
let expr = actualExpression.cast { $0 as? String }
|
||||
return try beginWith(expected as! String).matches(expr, failureMessage: failureMessage)
|
||||
// swiftlint:disable:next force_cast
|
||||
return try beginWith(expected as! String).satisfies(expr).toObjectiveC()
|
||||
} else {
|
||||
let expr = actualExpression.cast { $0 as? NMBOrderedCollection }
|
||||
return try beginWith(expected).matches(expr, failureMessage: failureMessage)
|
||||
return try beginWith(expected).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual sequence contains the expected value.
|
||||
/// A Nimble matcher that succeeds when the actual sequence contains the expected values.
|
||||
public func contain<S: Sequence, T: Equatable>(_ items: T...) -> Predicate<S>
|
||||
where S.Iterator.Element == T {
|
||||
where S.Element == T {
|
||||
return contain(items)
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual sequence contains the expected values.
|
||||
public func contain<S: Sequence, T: Equatable>(_ items: [T]) -> Predicate<S>
|
||||
where S.Iterator.Element == T {
|
||||
where S.Element == T {
|
||||
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
|
||||
if let actual = try actualExpression.evaluate() {
|
||||
let matches = items.all {
|
||||
let matches = items.allSatisfy {
|
||||
return actual.contains($0)
|
||||
}
|
||||
return PredicateStatus(bool: matches)
|
||||
@@ -19,6 +20,46 @@ public func contain<S: Sequence, T: Equatable>(_ items: [T]) -> Predicate<S>
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual set contains the expected values.
|
||||
public func contain<S: SetAlgebra, T: Equatable>(_ items: T...) -> Predicate<S>
|
||||
where S.Element == T {
|
||||
return contain(items)
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual set contains the expected values.
|
||||
public func contain<S: SetAlgebra, T: Equatable>(_ items: [T]) -> Predicate<S>
|
||||
where S.Element == T {
|
||||
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
|
||||
if let actual = try actualExpression.evaluate() {
|
||||
let matches = items.allSatisfy {
|
||||
return actual.contains($0)
|
||||
}
|
||||
return PredicateStatus(bool: matches)
|
||||
}
|
||||
return .fail
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual set contains the expected values.
|
||||
public func contain<S: Sequence & SetAlgebra, T: Equatable>(_ items: T...) -> Predicate<S>
|
||||
where S.Element == T {
|
||||
return contain(items)
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual set contains the expected values.
|
||||
public func contain<S: Sequence & SetAlgebra, T: Equatable>(_ items: [T]) -> Predicate<S>
|
||||
where S.Element == T {
|
||||
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
|
||||
if let actual = try actualExpression.evaluate() {
|
||||
let matches = items.allSatisfy {
|
||||
return actual.contains($0)
|
||||
}
|
||||
return PredicateStatus(bool: matches)
|
||||
}
|
||||
return .fail
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual string contains the expected substring.
|
||||
public func contain(_ substrings: String...) -> Predicate<String> {
|
||||
return contain(substrings)
|
||||
@@ -27,7 +68,7 @@ public func contain(_ substrings: String...) -> Predicate<String> {
|
||||
public func contain(_ substrings: [String]) -> Predicate<String> {
|
||||
return Predicate.simple("contain <\(arrayAsString(substrings))>") { actualExpression in
|
||||
if let actual = try actualExpression.evaluate() {
|
||||
let matches = substrings.all {
|
||||
let matches = substrings.allSatisfy {
|
||||
let range = actual.range(of: $0)
|
||||
return range != nil && !range!.isEmpty
|
||||
}
|
||||
@@ -45,7 +86,7 @@ public func contain(_ substrings: NSString...) -> Predicate<NSString> {
|
||||
public func contain(_ substrings: [NSString]) -> Predicate<NSString> {
|
||||
return Predicate.simple("contain <\(arrayAsString(substrings))>") { actualExpression in
|
||||
if let actual = try actualExpression.evaluate() {
|
||||
let matches = substrings.all { actual.range(of: $0.description).length != 0 }
|
||||
let matches = substrings.allSatisfy { actual.range(of: $0.description).length != 0 }
|
||||
return PredicateStatus(bool: matches)
|
||||
}
|
||||
return .fail
|
||||
@@ -60,17 +101,17 @@ public func contain(_ items: Any?...) -> Predicate<NMBContainer> {
|
||||
public func contain(_ items: [Any?]) -> Predicate<NMBContainer> {
|
||||
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
|
||||
guard let actual = try actualExpression.evaluate() else { return .fail }
|
||||
let matches = items.all { item in
|
||||
let matches = items.allSatisfy { item in
|
||||
return item.map { actual.contains($0) } ?? false
|
||||
}
|
||||
return PredicateStatus(bool: matches)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func containMatcher(_ expected: [NSObject]) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func containMatcher(_ expected: [NSObject]) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let location = actualExpression.location
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
if let value = actualValue as? NMBContainer {
|
||||
@@ -78,17 +119,25 @@ extension NMBObjCMatcher {
|
||||
|
||||
// A straightforward cast on the array causes this to crash, so we have to cast the individual items
|
||||
let expectedOptionals: [Any?] = expected.map({ $0 as Any? })
|
||||
return try contain(expectedOptionals).matches(expr, failureMessage: failureMessage)
|
||||
return try contain(expectedOptionals).satisfies(expr).toObjectiveC()
|
||||
} else if let value = actualValue as? NSString {
|
||||
let expr = Expression(expression: ({ value as String }), location: location)
|
||||
return try contain(expected as! [String]).matches(expr, failureMessage: failureMessage)
|
||||
} else if actualValue != nil {
|
||||
// swiftlint:disable:next line_length
|
||||
failureMessage.postfixMessage = "contain <\(arrayAsString(expected))> (only works for NSArrays, NSSets, NSHashTables, and NSStrings)"
|
||||
} else {
|
||||
failureMessage.postfixMessage = "contain <\(arrayAsString(expected))>"
|
||||
// swiftlint:disable:next force_cast
|
||||
return try contain(expected as! [String]).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
return false
|
||||
|
||||
let message: ExpectationMessage
|
||||
if actualValue != nil {
|
||||
message = ExpectationMessage.expectedActualValueTo(
|
||||
// swiftlint:disable:next line_length
|
||||
"contain <\(arrayAsString(expected))> (only works for NSArrays, NSSets, NSHashTables, and NSStrings)"
|
||||
)
|
||||
} else {
|
||||
message = ExpectationMessage
|
||||
.expectedActualValueTo("contain <\(arrayAsString(expected))>")
|
||||
.appendedBeNilHint()
|
||||
}
|
||||
return NMBPredicateResult(status: .fail, message: message.toObjectiveC())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +24,22 @@ public func containElementSatisfying<S: Sequence, T>(_ predicate: @escaping ((T)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func containElementSatisfyingMatcher(_ predicate: @escaping ((NSObject) -> Bool)) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func containElementSatisfyingMatcher(_ predicate: @escaping ((NSObject) -> Bool)) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let value = try actualExpression.evaluate()
|
||||
guard let enumeration = value as? NSFastEnumeration else {
|
||||
// swiftlint:disable:next line_length
|
||||
failureMessage.postfixMessage = "containElementSatisfying must be provided an NSFastEnumeration object"
|
||||
failureMessage.actualValue = nil
|
||||
failureMessage.expected = ""
|
||||
failureMessage.to = ""
|
||||
return false
|
||||
let message = ExpectationMessage.fail(
|
||||
"containElementSatisfying must be provided an NSFastEnumeration object"
|
||||
)
|
||||
return NMBPredicateResult(status: .fail, message: message.toObjectiveC())
|
||||
}
|
||||
|
||||
let message = ExpectationMessage
|
||||
.expectedTo("find object in collection that satisfies predicate")
|
||||
.toObjectiveC()
|
||||
|
||||
var iterator = NSFastEnumerationIterator(enumeration)
|
||||
while let item = iterator.next() {
|
||||
guard let object = item as? NSObject else {
|
||||
@@ -45,13 +47,11 @@ public func containElementSatisfying<S: Sequence, T>(_ predicate: @escaping ((T)
|
||||
}
|
||||
|
||||
if predicate(object) {
|
||||
return true
|
||||
return NMBPredicateResult(status: .matches, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
failureMessage.actualValue = nil
|
||||
failureMessage.postfixMessage = "find object in collection that satisfies predicate"
|
||||
return false
|
||||
return NMBPredicateResult(status: .doesNotMatch, message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/// A Nimble matcher that succeeds when the actual sequence contain the same elements in the same order to the exepected sequence.
|
||||
public func elementsEqual<S: Sequence>(_ expectedValue: S?) -> Predicate<S> where S.Element: Equatable {
|
||||
// A matcher abstraction for https://developer.apple.com/documentation/swift/sequence/2949668-elementsequal
|
||||
return Predicate.define("elementsEqual <\(stringify(expectedValue))>") { (actualExpression, msg) in
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
switch (expectedValue, actualValue) {
|
||||
case (nil, _?):
|
||||
return PredicateResult(status: .fail, message: msg.appendedBeNilHint())
|
||||
case (nil, nil), (_, nil):
|
||||
return PredicateResult(status: .fail, message: msg)
|
||||
case (let expected?, let actual?):
|
||||
let matches = expected.elementsEqual(actual)
|
||||
return PredicateResult(bool: matches, message: msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,17 +50,18 @@ public func endWith(_ endingSubstring: String) -> Predicate<String> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func endWithMatcher(_ expected: Any) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func endWithMatcher(_ expected: Any) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let actual = try actualExpression.evaluate()
|
||||
if (actual as? String) != nil {
|
||||
if actual is String {
|
||||
let expr = actualExpression.cast { $0 as? String }
|
||||
return try endWith(expected as! String).matches(expr, failureMessage: failureMessage)
|
||||
// swiftlint:disable:next force_cast
|
||||
return try endWith(expected as! String).satisfies(expr).toObjectiveC()
|
||||
} else {
|
||||
let expr = actualExpression.cast { $0 as? NMBOrderedCollection }
|
||||
return try endWith(expected).matches(expr, failureMessage: failureMessage)
|
||||
return try endWith(expected).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,103 +7,30 @@ import Foundation
|
||||
public func equal<T: Equatable>(_ expectedValue: T?) -> Predicate<T> {
|
||||
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
let matches = actualValue == expectedValue && expectedValue != nil
|
||||
if expectedValue == nil || actualValue == nil {
|
||||
if expectedValue == nil && actualValue != nil {
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: msg.appendedBeNilHint()
|
||||
)
|
||||
}
|
||||
switch (expectedValue, actualValue) {
|
||||
case (nil, _?):
|
||||
return PredicateResult(status: .fail, message: msg.appendedBeNilHint())
|
||||
case (nil, nil), (_, nil):
|
||||
return PredicateResult(status: .fail, message: msg)
|
||||
case (let expected?, let actual?):
|
||||
let matches = expected == actual
|
||||
return PredicateResult(bool: matches, message: msg)
|
||||
}
|
||||
return PredicateResult(status: PredicateStatus(bool: matches), message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual value is equal to the expected value.
|
||||
/// Values can support equal by supporting the Equatable protocol.
|
||||
///
|
||||
/// @see beCloseTo if you want to match imprecise types (eg - floats, doubles).
|
||||
public func equal<T, C: Equatable>(_ expectedValue: [T: C]?) -> Predicate<[T: C]> {
|
||||
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
if expectedValue == nil || actualValue == nil {
|
||||
if expectedValue == nil && actualValue != nil {
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: msg.appendedBeNilHint()
|
||||
)
|
||||
}
|
||||
return PredicateResult(status: .fail, message: msg)
|
||||
}
|
||||
return PredicateResult(
|
||||
status: PredicateStatus(bool: expectedValue! == actualValue!),
|
||||
message: msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual collection is equal to the expected collection.
|
||||
/// Items must implement the Equatable protocol.
|
||||
public func equal<T: Equatable>(_ expectedValue: [T]?) -> Predicate<[T]> {
|
||||
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
if expectedValue == nil || actualValue == nil {
|
||||
if expectedValue == nil && actualValue != nil {
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: msg.appendedBeNilHint()
|
||||
)
|
||||
}
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: msg
|
||||
)
|
||||
}
|
||||
return PredicateResult(
|
||||
bool: expectedValue! == actualValue!,
|
||||
message: msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nimble matcher allowing comparison of collection with optional type
|
||||
public func equal<T: Equatable>(_ expectedValue: [T?]) -> Predicate<[T?]> {
|
||||
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
|
||||
if let actualValue = try actualExpression.evaluate() {
|
||||
let doesNotMatch = PredicateResult(
|
||||
status: .doesNotMatch,
|
||||
message: msg
|
||||
)
|
||||
|
||||
if expectedValue.count != actualValue.count {
|
||||
return doesNotMatch
|
||||
}
|
||||
|
||||
for (index, item) in actualValue.enumerated() {
|
||||
let otherItem = expectedValue[index]
|
||||
if item == nil && otherItem == nil {
|
||||
continue
|
||||
} else if item == nil && otherItem != nil {
|
||||
return doesNotMatch
|
||||
} else if item != nil && otherItem == nil {
|
||||
return doesNotMatch
|
||||
} else if item! != otherItem! {
|
||||
return doesNotMatch
|
||||
}
|
||||
}
|
||||
|
||||
return PredicateResult(
|
||||
status: .matches,
|
||||
message: msg
|
||||
)
|
||||
} else {
|
||||
guard let actualValue = try actualExpression.evaluate() else {
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: msg.appendedBeNilHint()
|
||||
)
|
||||
}
|
||||
|
||||
let matches = expectedValue == actualValue
|
||||
return PredicateResult(bool: matches, message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,44 +55,45 @@ private func equal<T>(_ expectedValue: Set<T>?, stringify: @escaping (Set<T>?) -
|
||||
var errorMessage: ExpectationMessage =
|
||||
.expectedActualValueTo("equal <\(stringify(expectedValue))>")
|
||||
|
||||
if let expectedValue = expectedValue {
|
||||
if let actualValue = try actualExpression.evaluate() {
|
||||
errorMessage = .expectedCustomValueTo(
|
||||
"equal <\(stringify(expectedValue))>",
|
||||
"<\(stringify(actualValue))>"
|
||||
)
|
||||
|
||||
if expectedValue == actualValue {
|
||||
return PredicateResult(
|
||||
status: .matches,
|
||||
message: errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
let missing = expectedValue.subtracting(actualValue)
|
||||
if missing.count > 0 {
|
||||
errorMessage = errorMessage.appended(message: ", missing <\(stringify(missing))>")
|
||||
}
|
||||
|
||||
let extra = actualValue.subtracting(expectedValue)
|
||||
if extra.count > 0 {
|
||||
errorMessage = errorMessage.appended(message: ", extra <\(stringify(extra))>")
|
||||
}
|
||||
return PredicateResult(
|
||||
status: .doesNotMatch,
|
||||
message: errorMessage
|
||||
)
|
||||
}
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: errorMessage.appendedBeNilHint()
|
||||
)
|
||||
} else {
|
||||
guard let expectedValue = expectedValue else {
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: errorMessage.appendedBeNilHint()
|
||||
)
|
||||
}
|
||||
|
||||
guard let actualValue = try actualExpression.evaluate() else {
|
||||
return PredicateResult(
|
||||
status: .fail,
|
||||
message: errorMessage.appendedBeNilHint()
|
||||
)
|
||||
}
|
||||
|
||||
errorMessage = .expectedCustomValueTo(
|
||||
"equal <\(stringify(expectedValue))>",
|
||||
"<\(stringify(actualValue))>"
|
||||
)
|
||||
|
||||
if expectedValue == actualValue {
|
||||
return PredicateResult(
|
||||
status: .matches,
|
||||
message: errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
let missing = expectedValue.subtracting(actualValue)
|
||||
if missing.count > 0 {
|
||||
errorMessage = errorMessage.appended(message: ", missing <\(stringify(missing))>")
|
||||
}
|
||||
|
||||
let extra = actualValue.subtracting(expectedValue)
|
||||
if extra.count > 0 {
|
||||
errorMessage = errorMessage.appended(message: ", extra <\(stringify(extra))>")
|
||||
}
|
||||
return PredicateResult(
|
||||
status: .doesNotMatch,
|
||||
message: errorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +137,7 @@ public func !=<T, C: Equatable>(lhs: Expectation<[T: C]>, rhs: [T: C]?) {
|
||||
lhs.toNot(equal(rhs))
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func equalMatcher(_ expected: NSObject) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
|
||||
@@ -7,7 +7,7 @@ import Foundation
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual Collection's count equals
|
||||
/// the expected value
|
||||
public func haveCount<T: Collection>(_ expectedValue: T.IndexDistance) -> Predicate<T> {
|
||||
public func haveCount<T: Collection>(_ expectedValue: Int) -> Predicate<T> {
|
||||
return Predicate.define { actualExpression in
|
||||
if let actualValue = try actualExpression.evaluate() {
|
||||
let message = ExpectationMessage
|
||||
@@ -45,20 +45,29 @@ public func haveCount(_ expectedValue: Int) -> Predicate<NMBCollection> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func haveCountMatcher(_ expected: NSNumber) -> NMBObjCMatcher {
|
||||
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
|
||||
@objc public class func haveCountMatcher(_ expected: NSNumber) -> NMBMatcher {
|
||||
return NMBPredicate { actualExpression in
|
||||
let location = actualExpression.location
|
||||
let actualValue = try actualExpression.evaluate()
|
||||
if let value = actualValue as? NMBCollection {
|
||||
let expr = Expression(expression: ({ value as NMBCollection}), location: location)
|
||||
return try haveCount(expected.intValue).matches(expr, failureMessage: failureMessage)
|
||||
} else if let actualValue = actualValue {
|
||||
failureMessage.postfixMessage = "get type of NSArray, NSSet, NSDictionary, or NSHashTable"
|
||||
failureMessage.actualValue = "\(String(describing: type(of: actualValue)))"
|
||||
return try haveCount(expected.intValue).satisfies(expr).toObjectiveC()
|
||||
}
|
||||
return false
|
||||
|
||||
let message: ExpectationMessage
|
||||
if let actualValue = actualValue {
|
||||
message = ExpectationMessage.expectedCustomValueTo(
|
||||
"get type of NSArray, NSSet, NSDictionary, or NSHashTable",
|
||||
"\(String(describing: type(of: actualValue)))"
|
||||
)
|
||||
} else {
|
||||
message = ExpectationMessage
|
||||
.expectedActualValueTo("have a collection with count \(stringify(expected.intValue))")
|
||||
.appendedBeNilHint()
|
||||
}
|
||||
return NMBPredicateResult(status: .fail, message: message.toObjectiveC())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public func match(_ expectedValue: String?) -> Predicate<String> {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func matchMatcher(_ expected: NSString) -> NMBMatcher {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
// `CGFloat` is in Foundation (swift-corelibs-foundation) on Linux.
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
import CoreGraphics
|
||||
#endif
|
||||
|
||||
@@ -28,7 +28,7 @@ extension Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
/// Objective-C interface to the Swift variant of Matcher.
|
||||
@objc public protocol NMBMatcher {
|
||||
func matches(_ actualBlock: @escaping () -> NSObject?, failureMessage: FailureMessage, location: SourceLocation) -> Bool
|
||||
@@ -41,7 +41,8 @@ public protocol NMBContainer {
|
||||
func contains(_ anObject: Any) -> Bool
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
// swiftlint:disable:next todo
|
||||
// FIXME: NSHashTable can not conform to NMBContainer since swift-DEVELOPMENT-SNAPSHOT-2016-04-25-a
|
||||
//extension NSHashTable : NMBContainer {} // Corelibs Foundation does not include this class yet
|
||||
#endif
|
||||
@@ -54,7 +55,7 @@ public protocol NMBCollection {
|
||||
var count: Int { get }
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NSHashTable: NMBCollection {} // Corelibs Foundation does not include these classes yet
|
||||
extension NSMapTable: NMBCollection {}
|
||||
#endif
|
||||
@@ -131,7 +132,7 @@ extension NSDate: TestOutputStringConvertible {
|
||||
/// beGreaterThan(), beGreaterThanOrEqualTo(), and equal() matchers.
|
||||
///
|
||||
/// Types that conform to Swift's Comparable protocol will work implicitly too
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
@objc public protocol NMBComparable {
|
||||
func NMB_compare(_ otherObject: NMBComparable!) -> ComparisonResult
|
||||
}
|
||||
@@ -144,11 +145,13 @@ public protocol NMBComparable {
|
||||
|
||||
extension NSNumber: NMBComparable {
|
||||
public func NMB_compare(_ otherObject: NMBComparable!) -> ComparisonResult {
|
||||
// swiftlint:disable:next force_cast
|
||||
return compare(otherObject as! NSNumber)
|
||||
}
|
||||
}
|
||||
extension NSString: NMBComparable {
|
||||
public func NMB_compare(_ otherObject: NMBComparable!) -> ComparisonResult {
|
||||
// swiftlint:disable:next force_cast
|
||||
return compare(otherObject as! String)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
// A workaround to SR-6419.
|
||||
extension NotificationCenter {
|
||||
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
|
||||
#if swift(>=4.0)
|
||||
#if swift(>=4.0.2)
|
||||
#else
|
||||
func addObserver(forName name: Notification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
|
||||
return addObserver(forName: name, object: obj, queue: queue, usingBlock: block)
|
||||
}
|
||||
#endif
|
||||
#elseif swift(>=3.2)
|
||||
#if swift(>=3.2.2)
|
||||
#else
|
||||
// swiftlint:disable:next line_length
|
||||
func addObserver(forName name: Notification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
|
||||
return addObserver(forName: name, object: obj, queue: queue, usingBlock: block)
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
// swiftlint:disable:next line_length
|
||||
func addObserver(forName name: Notification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
|
||||
return addObserver(forName: name, object: obj, queue: queue, usingBlock: block)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
internal class NotificationCollector {
|
||||
private(set) var observedNotifications: [Notification]
|
||||
private let notificationCenter: NotificationCenter
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
private var token: AnyObject?
|
||||
#else
|
||||
private var token: NSObjectProtocol?
|
||||
@@ -43,14 +16,14 @@ internal class NotificationCollector {
|
||||
|
||||
func startObserving() {
|
||||
// swiftlint:disable:next line_length
|
||||
self.token = self.notificationCenter.addObserver(forName: nil, object: nil, queue: nil, using: { [weak self] n in
|
||||
self.token = self.notificationCenter.addObserver(forName: nil, object: nil, queue: nil) { [weak self] notification in
|
||||
// linux-swift gets confused by .append(n)
|
||||
self?.observedNotifications.append(n)
|
||||
})
|
||||
self?.observedNotifications.append(notification)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
if let token = self.token {
|
||||
self.notificationCenter.removeObserver(token)
|
||||
}
|
||||
|
||||
@@ -218,6 +218,7 @@ extension Predicate: Matcher {
|
||||
extension Predicate {
|
||||
// Someday, make this public? Needs documentation
|
||||
internal func after(f: @escaping (Expression<T>, PredicateResult) throws -> PredicateResult) -> Predicate<T> {
|
||||
// swiftlint:disable:previous identifier_name
|
||||
return Predicate { actual -> PredicateResult in
|
||||
let result = try self.satisfies(actual)
|
||||
return try f(actual, result)
|
||||
@@ -241,7 +242,7 @@ extension Predicate {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
public typealias PredicateBlock = (_ actualExpression: Expression<NSObject>) throws -> NMBPredicateResult
|
||||
|
||||
public class NMBPredicate: NSObject {
|
||||
@@ -311,7 +312,7 @@ final public class NMBPredicateStatus: NSObject {
|
||||
public static let doesNotMatch: NMBPredicateStatus = NMBPredicateStatus(status: 1)
|
||||
public static let fail: NMBPredicateStatus = NMBPredicateStatus(status: 2)
|
||||
|
||||
public override var hashValue: Int { return self.status.hashValue }
|
||||
public override var hash: Int { return self.status.hashValue }
|
||||
|
||||
public override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let otherPredicate = object as? NMBPredicateStatus else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
// This matcher requires the Objective-C, and being built by Xcode rather than the Swift Package Manager
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
|
||||
/// A Nimble matcher that succeeds when the actual expression raises an
|
||||
/// exception with the specified name, reason, and/or userInfo.
|
||||
@@ -23,8 +23,12 @@ public func raiseException(
|
||||
exception = e
|
||||
}), finally: nil)
|
||||
|
||||
capture.tryBlock {
|
||||
_ = try! actualExpression.evaluate()
|
||||
do {
|
||||
try capture.tryBlockThrows {
|
||||
_ = try actualExpression.evaluate()
|
||||
}
|
||||
} catch {
|
||||
return PredicateResult(status: .fail, message: .fail("unexpected error thrown: <\(error)>"))
|
||||
}
|
||||
|
||||
let failureMessage = FailureMessage()
|
||||
@@ -118,10 +122,12 @@ internal func exceptionMatchesNonNilFieldsOrClosure(
|
||||
}
|
||||
|
||||
public class NMBObjCRaiseExceptionMatcher: NSObject, NMBMatcher {
|
||||
// swiftlint:disable identifier_name
|
||||
internal var _name: String?
|
||||
internal var _reason: String?
|
||||
internal var _userInfo: NSDictionary?
|
||||
internal var _block: ((NSException) -> Void)?
|
||||
// swiftlint:enable identifier_name
|
||||
|
||||
internal init(name: String?, reason: String?, userInfo: NSDictionary?, block: ((NSException) -> Void)?) {
|
||||
_name = name
|
||||
|
||||
@@ -39,7 +39,7 @@ public func && <T>(left: Predicate<T>, right: Predicate<T>) -> Predicate<T> {
|
||||
return satisfyAllOf(left, right)
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func satisfyAllOfMatcher(_ matchers: [NMBMatcher]) -> NMBPredicate {
|
||||
return NMBPredicate { actualExpression in
|
||||
@@ -60,8 +60,12 @@ extension NMBObjCMatcher {
|
||||
return predicate.satisfies({ try expression.evaluate() }, location: actualExpression.location).toSwift()
|
||||
} else {
|
||||
let failureMessage = FailureMessage()
|
||||
// swiftlint:disable:next line_length
|
||||
let success = matcher.matches({ try! expression.evaluate() }, failureMessage: failureMessage, location: actualExpression.location)
|
||||
let success = matcher.matches(
|
||||
// swiftlint:disable:next force_try
|
||||
{ try! expression.evaluate() },
|
||||
failureMessage: failureMessage,
|
||||
location: actualExpression.location
|
||||
)
|
||||
return PredicateResult(bool: success, message: failureMessage.toExpectationMessage())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public func || <T>(left: MatcherFunc<T>, right: MatcherFunc<T>) -> Predicate<T>
|
||||
return satisfyAnyOf(left, right)
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
extension NMBObjCMatcher {
|
||||
@objc public class func satisfyAnyOfMatcher(_ matchers: [NMBMatcher]) -> NMBPredicate {
|
||||
return NMBPredicate { actualExpression in
|
||||
@@ -68,8 +68,12 @@ extension NMBObjCMatcher {
|
||||
return predicate.satisfies({ try expression.evaluate() }, location: actualExpression.location).toSwift()
|
||||
} else {
|
||||
let failureMessage = FailureMessage()
|
||||
// swiftlint:disable:next line_length
|
||||
let success = matcher.matches({ try! expression.evaluate() }, failureMessage: failureMessage, location: actualExpression.location)
|
||||
let success = matcher.matches(
|
||||
// swiftlint:disable:next force_try
|
||||
{ try! expression.evaluate() },
|
||||
failureMessage: failureMessage,
|
||||
location: actualExpression.location
|
||||
)
|
||||
return PredicateResult(bool: success, message: failureMessage.toExpectationMessage())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
public func throwAssertion() -> Predicate<Void> {
|
||||
return Predicate { actualExpression in
|
||||
#if arch(x86_64) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if arch(x86_64) && canImport(Darwin) && !SWIFT_PACKAGE
|
||||
let message = ExpectationMessage.expectedTo("throw an assertion")
|
||||
|
||||
var actualError: Error?
|
||||
@@ -44,9 +44,8 @@ public func throwAssertion() -> Predicate<Void> {
|
||||
" conditional statement")
|
||||
#else
|
||||
fatalError("The throwAssertion Nimble matcher can only run on x86_64 platforms with " +
|
||||
"Objective-C (e.g. Mac, iPhone 5s or later simulators). You can silence this error " +
|
||||
"by placing the test case inside an #if arch(x86_64) or (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) conditional statement")
|
||||
// swiftlint:disable:previous line_length
|
||||
"Objective-C (e.g. macOS, iPhone 5s or later simulators). You can silence this error " +
|
||||
"by placing the test case inside an #if arch(x86_64) or canImport(Darwin) conditional statement")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import CoreFoundation
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
|
||||
#if canImport(CDispatch)
|
||||
import CDispatch
|
||||
#endif
|
||||
|
||||
@@ -32,7 +32,7 @@ internal class AssertionWaitLock: WaitLock {
|
||||
|
||||
func acquireWaitingLock(_ fnName: String, file: FileString, line: UInt) {
|
||||
let info = WaitingInfo(name: fnName, file: file, lineNumber: line)
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
let isMainThread = Thread.isMainThread
|
||||
#else
|
||||
let isMainThread = _CFIsMainThread()
|
||||
@@ -45,10 +45,15 @@ internal class AssertionWaitLock: WaitLock {
|
||||
nimblePrecondition(
|
||||
currentWaiter == nil,
|
||||
"InvalidNimbleAPIUsage",
|
||||
"Nested async expectations are not allowed to avoid creating flaky tests.\n\n" +
|
||||
"The call to\n\t\(info)\n" +
|
||||
"triggered this exception because\n\t\(currentWaiter!)\n" +
|
||||
"is currently managing the main run loop."
|
||||
"""
|
||||
Nested async expectations are not allowed to avoid creating flaky tests.
|
||||
|
||||
The call to
|
||||
\t\(info)
|
||||
triggered this exception because
|
||||
\t\(currentWaiter!)
|
||||
is currently managing the main run loop.
|
||||
"""
|
||||
)
|
||||
currentWaiter = info
|
||||
}
|
||||
@@ -180,25 +185,18 @@ internal class AwaitPromiseBuilder<T> {
|
||||
// checked.
|
||||
//
|
||||
// In addition, stopping the run loop is used to halt code executed on the main run loop.
|
||||
#if swift(>=4.0)
|
||||
trigger.timeoutSource.schedule(
|
||||
deadline: DispatchTime.now() + timeoutInterval,
|
||||
repeating: .never,
|
||||
leeway: timeoutLeeway
|
||||
)
|
||||
#else
|
||||
trigger.timeoutSource.scheduleOneshot(
|
||||
deadline: DispatchTime.now() + timeoutInterval,
|
||||
leeway: timeoutLeeway
|
||||
)
|
||||
#endif
|
||||
trigger.timeoutSource.setEventHandler {
|
||||
guard self.promise.asyncResult.isIncomplete() else { return }
|
||||
let timedOutSem = DispatchSemaphore(value: 0)
|
||||
let semTimedOutOrBlocked = DispatchSemaphore(value: 0)
|
||||
semTimedOutOrBlocked.signal()
|
||||
let runLoop = CFRunLoopGetMain()
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
let runLoopMode = CFRunLoopMode.defaultMode.rawValue
|
||||
#else
|
||||
let runLoopMode = kCFRunLoopDefaultMode
|
||||
@@ -263,7 +261,7 @@ internal class AwaitPromiseBuilder<T> {
|
||||
self.trigger.timeoutSource.resume()
|
||||
while self.promise.asyncResult.isIncomplete() {
|
||||
// Stopping the run loop does not work unless we run only 1 mode
|
||||
#if swift(>=4.2)
|
||||
#if (swift(>=4.2) && canImport(Darwin)) || compiler(>=5.0)
|
||||
_ = RunLoop.current.run(mode: .default, before: .distantFuture)
|
||||
#else
|
||||
_ = RunLoop.current.run(mode: .defaultRunLoopMode, before: .distantFuture)
|
||||
@@ -333,11 +331,7 @@ internal class Awaiter {
|
||||
let asyncSource = createTimerSource(asyncQueue)
|
||||
let trigger = AwaitTrigger(timeoutSource: timeoutSource, actionSource: asyncSource) {
|
||||
let interval = DispatchTimeInterval.nanoseconds(Int(pollInterval * TimeInterval(NSEC_PER_SEC)))
|
||||
#if swift(>=4.0)
|
||||
asyncSource.schedule(deadline: .now(), repeating: interval, leeway: pollLeeway)
|
||||
#else
|
||||
asyncSource.scheduleRepeating(deadline: .now(), interval: interval, leeway: pollLeeway)
|
||||
#endif
|
||||
asyncSource.setEventHandler {
|
||||
do {
|
||||
if let result = try closure() {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
#if !swift(>=4.2)
|
||||
extension Sequence {
|
||||
internal func all(_ fn: (Iterator.Element) -> Bool) -> Bool {
|
||||
internal func allSatisfy(_ predicate: (Element) throws -> Bool) rethrows -> Bool {
|
||||
for item in self {
|
||||
if !fn(item) {
|
||||
if try !predicate(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
internal func identityAsString(_ value: Any?) -> String {
|
||||
let anyObject: AnyObject?
|
||||
#if os(Linux)
|
||||
#if os(Linux) && !swift(>=4.1.50)
|
||||
anyObject = value as? AnyObject
|
||||
#else
|
||||
anyObject = value as AnyObject?
|
||||
@@ -122,6 +122,7 @@ extension String: TestOutputStringConvertible {
|
||||
extension Data: TestOutputStringConvertible {
|
||||
public var testDescription: String {
|
||||
#if os(Linux)
|
||||
// swiftlint:disable:next todo
|
||||
// FIXME: Swift on Linux triggers a segfault when calling NSData's hash() (last checked on 03-11-16)
|
||||
return "Data<length=\(count)>"
|
||||
#else
|
||||
@@ -158,7 +159,7 @@ public func stringify<T>(_ value: T?) -> String {
|
||||
return String(describing: value)
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
@objc public class NMBStringer: NSObject {
|
||||
@objc public class func stringify(_ obj: Any?) -> String {
|
||||
return Nimble.stringify(obj)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[](https://cocoapods.org/pods/Quick)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/Quick)
|
||||
[](https://houndci.com)
|
||||
|
||||
Quick is a behavior-driven development framework for Swift and Objective-C.
|
||||
Inspired by [RSpec](https://github.com/rspec/rspec), [Specta](https://github.com/specta/specta), and [Ginkgo](https://github.com/onsi/ginkgo).
|
||||
@@ -44,7 +45,8 @@ Certain versions of Quick and Nimble only support certain versions of Swift. Dep
|
||||
|
||||
|Swift version |Quick version |Nimble version |
|
||||
|:--------------------|:---------------|:--------------|
|
||||
|Swift 3 |v1.0.0 or later |v5.0.0 or later|
|
||||
|Swift 4.2 |v1.3.2 or later |v7.3.2 or later|
|
||||
|Swift 3 / Swift 4 |v1.0.0 or later |v5.0.0 or later|
|
||||
|Swift 2.2 / Swift 2.3|v0.9.3 |v4.1.0 |
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
open class Behavior<Context> {
|
||||
|
||||
open static var name: String { return String(describing: self) }
|
||||
public static var name: String { return String(describing: self) }
|
||||
/**
|
||||
override this method in your behavior to define a set of reusable examples.
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
|
||||
// does not work as expected.
|
||||
#if swift(>=3.2)
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _CallsiteBase: NSObject {}
|
||||
#else
|
||||
public class _CallsiteBase: NSObject {}
|
||||
#endif
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _CallsiteBase: NSObject {}
|
||||
#else
|
||||
public class _CallsiteBase: NSObject {}
|
||||
#endif
|
||||
|
||||
@@ -72,7 +72,7 @@ final public class Configuration: NSObject {
|
||||
provided with metadata on the example that the closure is being run
|
||||
prior to.
|
||||
*/
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
@objc(beforeEachWithMetadata:)
|
||||
public func beforeEach(_ closure: @escaping BeforeExampleWithMetadataClosure) {
|
||||
exampleHooks.appendBefore(closure)
|
||||
@@ -109,7 +109,7 @@ final public class Configuration: NSObject {
|
||||
is provided with metadata on the example that the closure is being
|
||||
run after.
|
||||
*/
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
@objc(afterEachWithMetadata:)
|
||||
public func afterEach(_ closure: @escaping AfterExampleWithMetadataClosure) {
|
||||
exampleHooks.appendAfter(closure)
|
||||
|
||||
@@ -56,7 +56,7 @@ extension World {
|
||||
currentExampleGroup.hooks.appendBefore(closure)
|
||||
}
|
||||
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objc(beforeEachWithMetadata:)
|
||||
internal func beforeEach(closure: @escaping BeforeExampleWithMetadataClosure) {
|
||||
currentExampleGroup.hooks.appendBefore(closure)
|
||||
@@ -74,7 +74,7 @@ extension World {
|
||||
currentExampleGroup.hooks.appendAfter(closure)
|
||||
}
|
||||
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objc(afterEachWithMetadata:)
|
||||
internal func afterEach(closure: @escaping AfterExampleWithMetadataClosure) {
|
||||
currentExampleGroup.hooks.appendAfter(closure)
|
||||
@@ -172,7 +172,7 @@ extension World {
|
||||
self.itBehavesLike(behavior, context: context, flags: pendingFlags, file: file, line: line)
|
||||
}
|
||||
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objc(itWithDescription:flags:file:line:closure:)
|
||||
internal func objc_it(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
|
||||
it(description, flags: flags, file: file, line: line, closure: closure)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
internal func raiseError(_ message: String) -> Never {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
NSException(name: .internalInconsistencyException, reason: message, userInfo: nil).raise()
|
||||
#endif
|
||||
|
||||
|
||||
@@ -3,15 +3,9 @@ import Foundation
|
||||
private var numberOfExamplesRun = 0
|
||||
private var numberOfIncludedExamples = 0
|
||||
|
||||
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
|
||||
// does not work as expected.
|
||||
#if swift(>=3.2)
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _ExampleBase: NSObject {}
|
||||
#else
|
||||
public class _ExampleBase: NSObject {}
|
||||
#endif
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _ExampleBase: NSObject {}
|
||||
#else
|
||||
public class _ExampleBase: NSObject {}
|
||||
#endif
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
|
||||
// does not work as expected.
|
||||
#if swift(>=3.2)
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _ExampleMetadataBase: NSObject {}
|
||||
#else
|
||||
public class _ExampleMetadataBase: NSObject {}
|
||||
#endif
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _ExampleMetadataBase: NSObject {}
|
||||
#else
|
||||
public class _ExampleMetadataBase: NSObject {}
|
||||
#endif
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
|
||||
// does not work as expected.
|
||||
#if swift(>=3.2)
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _FilterBase: NSObject {}
|
||||
#else
|
||||
public class _FilterBase: NSObject {}
|
||||
#endif
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
public class _FilterBase: NSObject {}
|
||||
#else
|
||||
public class _FilterBase: NSObject {}
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#if canImport(Darwin)
|
||||
import Foundation
|
||||
|
||||
extension NSString {
|
||||
@@ -21,7 +21,7 @@ extension NSString {
|
||||
return invalidCharacters
|
||||
}()
|
||||
|
||||
/// This API is not meant to be used outside Quick, so will be unavaialbe in
|
||||
/// This API is not meant to be used outside Quick, so will be unavailable in
|
||||
/// a next major version.
|
||||
@objc(qck_c99ExtendedIdentifier)
|
||||
public var c99ExtendedIdentifier: String {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#if canImport(Darwin)
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
import XCTest
|
||||
|
||||
|
||||
@@ -12,15 +12,9 @@ public typealias SharedExampleContext = () -> [String: Any]
|
||||
*/
|
||||
public typealias SharedExampleClosure = (@escaping SharedExampleContext) -> Void
|
||||
|
||||
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
|
||||
// does not work as expected.
|
||||
#if swift(>=3.2)
|
||||
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
internal class _WorldBase: NSObject {}
|
||||
#else
|
||||
internal class _WorldBase: NSObject {}
|
||||
#endif
|
||||
#if canImport(Darwin) && !SWIFT_PACKAGE
|
||||
@objcMembers
|
||||
internal class _WorldBase: NSObject {}
|
||||
#else
|
||||
internal class _WorldBase: NSObject {}
|
||||
#endif
|
||||
@@ -57,7 +51,7 @@ final internal class World: _WorldBase {
|
||||
within this test suite. This is only true within the context of Quick
|
||||
functional tests.
|
||||
*/
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
// Convention of generating Objective-C selector has been changed on Swift 3
|
||||
@objc(isRunningAdditionalSuites)
|
||||
internal var isRunningAdditionalSuites = false
|
||||
@@ -158,7 +152,7 @@ final internal class World: _WorldBase {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(Darwin)
|
||||
@objc(examplesForSpecClass:)
|
||||
internal func objc_examples(_ specClass: AnyClass) -> [Example] {
|
||||
return examples(specClass)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>7.3.1</string>
|
||||
<string>8.0.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.2</string>
|
||||
<string>2.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.3</string>
|
||||
<string>0.7.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 1010;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 1;
|
||||
@@ -317,14 +317,14 @@
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 1010;
|
||||
LastSwiftMigration = 1020;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudio" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
@@ -630,7 +630,7 @@
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -647,7 +647,7 @@
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
@@ -669,7 +669,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -687,7 +687,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -31,6 +31,7 @@ class ViewController: UIViewController {
|
||||
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
|
||||
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
|
||||
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
|
||||
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
|
||||
}
|
||||
|
||||
@IBAction func togglePlay(_ sender: Any) {
|
||||
@@ -116,4 +117,8 @@ class ViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
try? controller.audioSessionController.set(category: .playback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,10 +20,15 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
|
||||
beforeEach {
|
||||
player = AVPlayer()
|
||||
player.volume = 0.0
|
||||
observer = AVPlayerObserver(player: player)
|
||||
observer = AVPlayerObserver()
|
||||
observer.player = player
|
||||
observer.delegate = self
|
||||
}
|
||||
|
||||
it("should not be observing", closure: {
|
||||
expect(observer.isObserving).to(beFalse())
|
||||
})
|
||||
|
||||
context("when observing has started", {
|
||||
beforeEach {
|
||||
observer.startObserving()
|
||||
@@ -54,6 +59,17 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
|
||||
expect(observer.isObserving).toEventually(beTrue())
|
||||
})
|
||||
})
|
||||
|
||||
context("when stopping observing", closure: {
|
||||
|
||||
beforeEach {
|
||||
observer.stopObserving()
|
||||
}
|
||||
|
||||
it("should not be observing", closure: {
|
||||
expect(observer.isObserving).to(beFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ class AVPlayerTimeObserverTests: QuickSpec {
|
||||
player = AVPlayer()
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
player.volume = 0
|
||||
observer = AVPlayerTimeObserver(player: player, periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
|
||||
observer = AVPlayerTimeObserver(periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
|
||||
observer.player = player
|
||||
}
|
||||
|
||||
context("has started boundary time observing", {
|
||||
|
||||
@@ -1,240 +1,186 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
|
||||
class AVPlayerWrapperTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An AVPlayerWrapper") {
|
||||
|
||||
var wrapper: AVPlayerWrapper!
|
||||
|
||||
beforeEach {
|
||||
let player = AVPlayer()
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
player.volume = 0.0
|
||||
wrapper = AVPlayerWrapper(avPlayer: player)
|
||||
wrapper.bufferDuration = 0.0001
|
||||
class AVPlayerWrapperTests: XCTestCase {
|
||||
|
||||
var wrapper: AVPlayerWrapper!
|
||||
var holder: AVPlayerWrapperDelegateHolder!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
wrapper = AVPlayerWrapper()
|
||||
wrapper.volume = 0.0
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = false
|
||||
holder = AVPlayerWrapperDelegateHolder()
|
||||
wrapper.delegate = holder
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
wrapper = nil
|
||||
holder = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - State tests
|
||||
|
||||
func test_AVPlayerWrapper__state__should_be_idle() {
|
||||
XCTAssert(wrapper.state == AVPlayerWrapperState.idle)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_loading_a_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if state == .ready {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
describe("its state", {
|
||||
it("should be idle", closure: {
|
||||
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
|
||||
})
|
||||
|
||||
context("when loading a source", {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should eventually be ready", closure: {
|
||||
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
|
||||
})
|
||||
})
|
||||
|
||||
context("when playing with no source", {
|
||||
beforeEach {
|
||||
wrapper.play()
|
||||
}
|
||||
it("should be idle", closure: {
|
||||
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
|
||||
})
|
||||
})
|
||||
|
||||
context("when playing a source", {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be playing", closure: {
|
||||
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.playing))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
context("when pausing the source", {
|
||||
|
||||
let holder = AVPlayerWrapperDelegateHolder()
|
||||
|
||||
beforeEach {
|
||||
wrapper.delegate = holder
|
||||
holder.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
wrapper.pause()
|
||||
}
|
||||
}
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be paused", closure: {
|
||||
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
|
||||
})
|
||||
})
|
||||
|
||||
context("when toggling the source from play", {
|
||||
let holder = AVPlayerWrapperDelegateHolder()
|
||||
beforeEach {
|
||||
wrapper.delegate = holder
|
||||
holder.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
wrapper.togglePlaying()
|
||||
}
|
||||
}
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
|
||||
}
|
||||
it("should eventually be paused", closure: {
|
||||
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
|
||||
})
|
||||
})
|
||||
|
||||
context("when stopping the source", {
|
||||
|
||||
var holder: AVPlayerWrapperDelegateHolder!
|
||||
var receivedIdleUpdate: Bool = false
|
||||
|
||||
beforeEach {
|
||||
holder = AVPlayerWrapperDelegateHolder()
|
||||
wrapper.delegate = holder
|
||||
holder.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
wrapper.stop()
|
||||
}
|
||||
if state == .idle {
|
||||
receivedIdleUpdate = true
|
||||
}
|
||||
}
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be 'idle'", closure: {
|
||||
expect(receivedIdleUpdate).toEventually(beTrue())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
context("when seeking before loading", {
|
||||
beforeEach {
|
||||
wrapper.seek(to: 10)
|
||||
}
|
||||
it("should be idle", closure: {
|
||||
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
|
||||
})
|
||||
})
|
||||
|
||||
context("when loading source with initial time", closure: {
|
||||
let initialTime: TimeInterval = 4.0
|
||||
beforeEach {
|
||||
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: initialTime)
|
||||
}
|
||||
|
||||
it("should eventually be playing", closure: {
|
||||
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.playing))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("its duration", {
|
||||
it("should be 0", closure: {
|
||||
expect(wrapper.duration).to(equal(0))
|
||||
})
|
||||
|
||||
context("when loading source", {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: LongSource.path), playWhenReady: false)
|
||||
}
|
||||
it("should eventually not be 0", closure: {
|
||||
expect(wrapper.duration).toEventuallyNot(equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("its current time", {
|
||||
it("should be 0", closure: {
|
||||
expect(wrapper.currentTime).to(equal(0))
|
||||
})
|
||||
|
||||
context("when seeking to a time", {
|
||||
let holder = AVPlayerWrapperDelegateHolder()
|
||||
let seekTime: TimeInterval = 0.5
|
||||
beforeEach {
|
||||
wrapper.delegate = holder
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
}
|
||||
|
||||
it("should eventually be equal to the seeked time", closure: {
|
||||
expect(wrapper.currentTime).toEventually(equal(seekTime))
|
||||
})
|
||||
})
|
||||
|
||||
context("when playing from initial time", closure: {
|
||||
let initialTime: TimeInterval = 4.0
|
||||
beforeEach {
|
||||
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: initialTime)
|
||||
}
|
||||
|
||||
it("should eventuallt be equal to the initial time", closure: {
|
||||
expect(wrapper.currentTime).toEventually(equal(initialTime))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("its rate", {
|
||||
it("should be 0", closure: {
|
||||
expect(wrapper.rate).to(equal(0.0))
|
||||
})
|
||||
|
||||
context("when playing a source", {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be 1.0", closure: {
|
||||
expect(wrapper.rate).toEventually(equal(1.0))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe("its automaticallyWaitsToMinimizeStalling option", {
|
||||
it("should be false", closure: {
|
||||
expect(wrapper.automaticallyWaitsToMinimizeStalling).to(beFalse())
|
||||
})
|
||||
|
||||
context("when setting it to true", {
|
||||
beforeEach {
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = true
|
||||
}
|
||||
|
||||
it("should be true", closure: {
|
||||
expect(wrapper.automaticallyWaitsToMinimizeStalling).to(beTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("its timeEventFrequency", {
|
||||
context("when updated", {
|
||||
beforeEach {
|
||||
wrapper.timeEventFrequency = .everyHalfSecond
|
||||
}
|
||||
|
||||
it("should update the playerTimeObservers periodicObserverTimeInterval", closure: {
|
||||
expect(wrapper.playerTimeObserver.periodicObserverTimeInterval).to(equal(TimeEventFrequency.everyHalfSecond.getTime()))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_playing_a_source__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if state == .playing {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_pausing_a_source__should_be_paused() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.pause()
|
||||
case .paused: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_toggling_from_play__should_be_paused() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.togglePlaying()
|
||||
case .paused: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_stopping__should_be_stopped() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.stop()
|
||||
case .idle: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__loading_with_intial_time__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: 4.0)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Duration tests
|
||||
|
||||
func test_AVPlayerWrapper__duration__should_be_0() {
|
||||
XCTAssert(wrapper.duration == 0.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__duration__loading_a_source__should_not_be_0() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { _ in
|
||||
if self.wrapper.duration > 0 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Current time tests
|
||||
|
||||
func test_AVPlayerWrapper__currentTime__should_be_0() {
|
||||
XCTAssert(wrapper.currentTime == 0)
|
||||
}
|
||||
|
||||
// MARK: - Seeking
|
||||
|
||||
func test_AVPlayerWrapper__seeking__should_seek() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
self.wrapper.seek(to: seekTime)
|
||||
}
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: 4.0)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Rate tests
|
||||
|
||||
func test_AVPlayerWrapper__rate__should_be_0() {
|
||||
XCTAssert(wrapper.rate == 0.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__rate__playing_a_source__should_be_1() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if self.wrapper.rate == 1.0 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__timeObserver__when_updated__should_update_the_observers_periodicObserverTimeInterval() {
|
||||
wrapper.timeEventFrequency = .everySecond
|
||||
XCTAssert(wrapper.playerTimeObserver.periodicObserverTimeInterval == TimeEventFrequency.everySecond.getTime())
|
||||
wrapper.timeEventFrequency = .everyHalfSecond
|
||||
XCTAssert(wrapper.playerTimeObserver.periodicObserverTimeInterval == TimeEventFrequency.everyHalfSecond.getTime())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
|
||||
}
|
||||
@@ -248,6 +194,8 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
}
|
||||
|
||||
var stateUpdate: ((_ state: AVPlayerWrapperState) -> Void)?
|
||||
var didUpdateDuration: ((_ duration: Double) -> Void)?
|
||||
var didSeekTo: ((_ seconds: Int) -> Void)?
|
||||
var itemDidComplete: (() -> Void)?
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
@@ -262,16 +210,15 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
|
||||
}
|
||||
|
||||
var seekCompletion: (() -> Void)?
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
seekCompletion?()
|
||||
didSeekTo?(seconds)
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
if let state = self.state {
|
||||
self.stateUpdate?(state)
|
||||
|
||||
}
|
||||
didUpdateDuration?(duration)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,191 +1,178 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
class AudioPlayerTests: QuickSpec {
|
||||
class AudioPlayerTests: XCTestCase {
|
||||
|
||||
override func spec() {
|
||||
describe("An AudioPlayer") {
|
||||
var audioPlayer: AudioPlayer!
|
||||
|
||||
beforeEach {
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.bufferDuration = 0.0001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
audioPlayer.volume = 0.0
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
audioPlayer.bufferDuration = 0.001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
|
||||
describe("its state", {
|
||||
|
||||
it("should be idle", closure: {
|
||||
expect(audioPlayer.playerState).to(equal(AudioPlayerState.idle))
|
||||
})
|
||||
|
||||
context("when audio item is loaded", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("it should eventually be ready", closure: {
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
|
||||
})
|
||||
})
|
||||
|
||||
context("when an item is loaded (playWhenReady=true)", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("it should eventually be playing", closure: {
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
})
|
||||
})
|
||||
|
||||
context("when playing an item", {
|
||||
var listener: AudioPlayerEventListener!
|
||||
beforeEach {
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
listener.stateUpdate = { state in
|
||||
if state == .ready {
|
||||
audioPlayer.play()
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should eventually be playing", closure: {
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
})
|
||||
})
|
||||
|
||||
context("when pausing an item", {
|
||||
var listener: AudioPlayerEventListener!
|
||||
beforeEach {
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
listener.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
audioPlayer.pause()
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be paused", closure: {
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
|
||||
})
|
||||
})
|
||||
|
||||
context("when stopping an item", {
|
||||
var listener: AudioPlayerEventListener!
|
||||
beforeEach {
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
listener.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be idle", closure: {
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.idle))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("its current time", {
|
||||
it("should be 0", closure: {
|
||||
expect(audioPlayer.currentTime).to(equal(0))
|
||||
})
|
||||
|
||||
context("when seeking to a time", {
|
||||
let seekTime: TimeInterval = 1.0
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: seekTime)
|
||||
}
|
||||
|
||||
it("should eventually be equal to the seeked time", closure: {
|
||||
expect(audioPlayer.currentTime).toEventually(equal(seekTime))
|
||||
})
|
||||
})
|
||||
|
||||
context("when playing an item with an initial time", {
|
||||
var item: DefaultAudioItemInitialTime!
|
||||
beforeEach {
|
||||
item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should eventaully be equal to the initial time", closure: {
|
||||
expect(audioPlayer.currentTime).toEventually(equal(item.getInitialTime()))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("its rate", {
|
||||
it("should be 0", closure: {
|
||||
expect(audioPlayer.rate).to(equal(0))
|
||||
})
|
||||
|
||||
context("when playing an item", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be 1.0", closure: {
|
||||
expect(audioPlayer.rate).toEventually(equal(1.0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("its currentItem", {
|
||||
it("should be nil", closure: {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
})
|
||||
|
||||
context("when loading an item", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should not be nil", closure: {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
})
|
||||
})
|
||||
|
||||
context("when setting the timePitchAlgorithm", {
|
||||
|
||||
beforeEach {
|
||||
audioPlayer.audioTimePitchAlgorithm = .timeDomain
|
||||
}
|
||||
|
||||
context("then loading an item", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have the applied timePitchAlgorithm", closure: {
|
||||
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.timeDomain))
|
||||
})
|
||||
})
|
||||
|
||||
context("then loading a timepitching item", {
|
||||
beforeEach {
|
||||
let item = DefaultAudioItemTimePitching(audioUrl: Source.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm.spectral)
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have the applied timePitchAlgorithm", closure: {
|
||||
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.spectral))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source_playWhenReady__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__play_source__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: self.audioPlayer.play()
|
||||
case .playing: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__pausing_source__should_be_paused() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.audioPlayer.pause()
|
||||
case .paused: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__stopping_source__should_be_idle() {
|
||||
let expectation = XCTestExpectation()
|
||||
var hasBeenPlaying: Bool = false
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing:
|
||||
hasBeenPlaying = true
|
||||
self.audioPlayer.stop()
|
||||
case .idle:
|
||||
if hasBeenPlaying {
|
||||
expectation.fulfill()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Current time
|
||||
|
||||
func test_AudioPlayer__currentTime__should_be_0() {
|
||||
XCTAssert(audioPlayer.currentTime == 0.0)
|
||||
}
|
||||
|
||||
// Commented out -- Keeps failing in CI at Bitrise, but succeeds locally, even with Bitrise CLI.
|
||||
// func test_AudioPlayer__currentTime__playing_source__shold_be_greater_than_0() {
|
||||
// let expectation = XCTestExpectation()
|
||||
// audioPlayer.timeEventFrequency = .everyQuarterSecond
|
||||
// listener.secondsElapse = { _ in
|
||||
// if self.audioPlayer.currentTime > 0.0 {
|
||||
// expectation.fulfill()
|
||||
// }
|
||||
// }
|
||||
// try? audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
// wait(for: [expectation], timeout: 20.0)
|
||||
// }
|
||||
|
||||
func test_AudioPlayer__currentTime__when_loading_source_with_intial_time__should_be_equal_to_initial_time() {
|
||||
let expectation = XCTestExpectation()
|
||||
let item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
if self.audioPlayer.currentTime == item.getInitialTime() {
|
||||
expectation.fulfill()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Rate
|
||||
|
||||
func test_AudioPlayer__rate__should_be_0() {
|
||||
XCTAssert(audioPlayer.rate == 0.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__rate__playing_source__should_be_1() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing:
|
||||
if self.audioPlayer.rate == 1.0 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Current item
|
||||
|
||||
func test_AudioPlayer__currentItem__should_be_nil() {
|
||||
XCTAssertNil(audioPlayer.currentItem)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
if self.audioPlayer.currentItem != nil {
|
||||
expectation.fulfill()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -201,11 +188,13 @@ class AudioPlayerEventListener {
|
||||
}
|
||||
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
|
||||
var seekCompletion: (() -> Void)?
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
|
||||
audioPlayer.event.seek.addListener(self, handleSeek)
|
||||
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
|
||||
}
|
||||
|
||||
func handleDidUpdateState(state: AudioPlayerState) {
|
||||
@@ -216,4 +205,8 @@ class AudioPlayerEventListener {
|
||||
seekCompletion?()
|
||||
}
|
||||
|
||||
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
|
||||
self.secondsElapse?(data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# SwiftAudio
|
||||
|
||||
[](https://app.bitrise.io/app/3d3ac2ba8d817235)
|
||||
@@ -23,13 +25,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'SwiftAudio', '~> 0.7.2'
|
||||
pod 'SwiftAudio', '~> 0.9.2'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
|
||||
```ruby
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.7.2
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.2
|
||||
```
|
||||
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
|
||||
|
||||
@@ -61,6 +63,18 @@ class MyCustomViewController: UIViewController {
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use the [Combine](https://developer.apple.com/documentation/combine) framework for events, each event in the `AudioPlayer` has an `EventPublisher` that can be subscribed to:
|
||||
```swift
|
||||
let audioPlayer = AudioPlayer()
|
||||
audioPlayer.event.stateChange.publisher.subscribe(subscriber: someSubscriber)
|
||||
|
||||
/// Using a Sink
|
||||
audioPlayer.event.stateChange.publisher.sink { state in
|
||||
/// Handle state change here.
|
||||
}
|
||||
```
|
||||
**Important**: This requires iOS version equal to or later than 13.0. If an application needs to support older iOS versions it is recommended to use the regular `Event.addListener(listener:)` method.
|
||||
|
||||
#### QueuedAudioPlayer
|
||||
The `QueuedAudioPlayer` is a subclass of `AudioPlayer` that maintains a queue of audio tracks.
|
||||
```swift
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.7.2'
|
||||
s.version = '0.9.2'
|
||||
s.summary = 'Easy audio streaming for iOS'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -10,18 +10,20 @@ import Foundation
|
||||
|
||||
public struct APError {
|
||||
|
||||
enum LoadError: Error {
|
||||
public enum LoadError: Error {
|
||||
case invalidSourceUrl(String)
|
||||
}
|
||||
|
||||
enum PlaybackError: Error {
|
||||
public enum PlaybackError: Error {
|
||||
case noLoadedItem
|
||||
}
|
||||
|
||||
enum QueueError: Error {
|
||||
public enum QueueError: Error {
|
||||
case noPreviousItem
|
||||
case noNextItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
}
|
||||
|
||||
public enum EventError: Error {}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let avPlayer: AVPlayer
|
||||
var avPlayer: AVPlayer
|
||||
let playerObserver: AVPlayerObserver
|
||||
let playerTimeObserver: AVPlayerTimeObserver
|
||||
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
||||
let playerItemObserver: AVPlayerItemObserver
|
||||
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
@@ -46,10 +46,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public init(avPlayer: AVPlayer = AVPlayer()) {
|
||||
self.avPlayer = avPlayer
|
||||
self.playerObserver = AVPlayerObserver(player: avPlayer)
|
||||
self.playerTimeObserver = AVPlayerTimeObserver(player: avPlayer, periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
public init() {
|
||||
self.avPlayer = AVPlayer()
|
||||
self.playerObserver = AVPlayerObserver()
|
||||
self.playerObserver.player = avPlayer
|
||||
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
self.playerTimeObserver.player = avPlayer
|
||||
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
self.playerItemObserver = AVPlayerItemObserver()
|
||||
|
||||
@@ -75,6 +77,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
return avPlayer.currentItem
|
||||
}
|
||||
|
||||
var _pendingAsset: AVAsset? = nil
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
@@ -84,7 +88,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
let seconds = avPlayer.currentTime().seconds
|
||||
return seconds.isNaN ? 0 : seconds
|
||||
}
|
||||
|
||||
|
||||
var duration: TimeInterval {
|
||||
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
@@ -102,7 +106,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
var bufferedPosition: TimeInterval {
|
||||
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
@@ -112,7 +116,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var rate: Float {
|
||||
get { return avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
@@ -142,6 +146,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
pause()
|
||||
case .paused:
|
||||
play()
|
||||
@unknown default:
|
||||
fatalError("Unknown AVPlayer.timeControlStatus")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,22 +167,57 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool) {
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
recreateAVPlayer()
|
||||
}
|
||||
|
||||
// Set item
|
||||
let currentAsset = AVURLAsset(url: url)
|
||||
let currentItem = AVPlayerItem(asset: currentAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
||||
currentItem.preferredForwardBufferDuration = bufferDuration
|
||||
avPlayer.replaceCurrentItem(with: currentItem)
|
||||
|
||||
// Register for events
|
||||
playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
playerObserver.startObserving()
|
||||
playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
playerItemObserver.startObserving(item: currentItem)
|
||||
self._pendingAsset = AVURLAsset(url: url)
|
||||
if let pendingAsset = _pendingAsset {
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: {
|
||||
var error: NSError? = nil
|
||||
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
|
||||
switch status {
|
||||
case .loaded:
|
||||
if isPendingAsset {
|
||||
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
||||
currentItem.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: currentItem)
|
||||
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
self.playerItemObserver.startObserving(item: currentItem)
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
// print("load asset failed")
|
||||
if isPendingAsset {
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
self._pendingAsset = nil
|
||||
}
|
||||
break
|
||||
|
||||
case .cancelled:
|
||||
// print("load asset cancelled")
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
|
||||
@@ -192,11 +233,26 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
|
||||
if self._pendingAsset != nil {
|
||||
self._pendingAsset?.cancelLoading()
|
||||
self._pendingAsset = nil
|
||||
}
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Will recreate the AVPlayer instance. Used when the current one fails.
|
||||
private func recreateAVPlayer() {
|
||||
let player = AVPlayer()
|
||||
playerObserver.player = player
|
||||
playerTimeObserver.player = player
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
avPlayer = player
|
||||
delegate?.AVWrapperDidRecreateAVPlayer()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
@@ -216,6 +272,8 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
self._state = .loading
|
||||
case .playing:
|
||||
self._state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,13 +291,15 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
|
||||
case .failed:
|
||||
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
|
||||
break
|
||||
|
||||
case .unknown:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@ protocol AVPlayerWrapperDelegate: class {
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerWrapperProtocol {
|
||||
protocol AVPlayerWrapperProtocol: class {
|
||||
|
||||
var state: AVPlayerWrapperState { get }
|
||||
|
||||
|
||||
@@ -10,23 +10,6 @@ import MediaPlayer
|
||||
|
||||
public typealias AudioPlayerState = AVPlayerWrapperState
|
||||
|
||||
@available(*, deprecated, message: "Delegates will be removed in future versions of SwiftAudio. Use event handlers instead.")
|
||||
public protocol AudioPlayerDelegate: class {
|
||||
|
||||
func audioPlayer(playerDidChangeState state: AudioPlayerState)
|
||||
|
||||
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason)
|
||||
|
||||
func audioPlayer(secondsElapsed seconds: Double)
|
||||
|
||||
func audioPlayer(failedWithError error: Error?)
|
||||
|
||||
func audioPlayer(seekTo seconds: Int, didFinish: Bool)
|
||||
|
||||
func audioPlayer(didUpdateDuration duration: Double)
|
||||
|
||||
}
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
private var _wrapper: AVPlayerWrapperProtocol
|
||||
@@ -38,9 +21,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
|
||||
public let event = EventHolder()
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
public var currentItem: AudioItem? {
|
||||
@@ -146,10 +127,9 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
|
||||
*/
|
||||
public init(avPlayer: AVPlayer = AVPlayer(),
|
||||
nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
|
||||
self._wrapper = AVPlayerWrapper()
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
@@ -183,13 +163,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
playWhenReady: playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime())
|
||||
|
||||
if let item = item as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
}
|
||||
else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
|
||||
self._currentItem = item
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
@@ -226,7 +199,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
self.reset()
|
||||
self.wrapper.stop()
|
||||
self.event.playbackEnd.emit(data: .playerStopped)
|
||||
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: .playerStopped)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,6 +292,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
self._currentItem = nil
|
||||
}
|
||||
|
||||
private func setTimePitchingAlgorithmForCurrentItem() {
|
||||
if let item = currentItem as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
}
|
||||
else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
@@ -328,6 +309,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
|
||||
setTimePitchingAlgorithmForCurrentItem()
|
||||
case .playing, .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
@@ -336,17 +319,14 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
default: break
|
||||
}
|
||||
self.event.stateChange.emit(data: state)
|
||||
self.delegate?.audioPlayer(playerDidChangeState: state)
|
||||
}
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
self.event.secondElapse.emit(data: seconds)
|
||||
self.delegate?.audioPlayer(secondsElapsed: seconds)
|
||||
}
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
self.event.fail.emit(data: error)
|
||||
self.delegate?.audioPlayer(failedWithError: error)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
@@ -354,17 +334,18 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
}
|
||||
self.event.seek.emit(data: (seconds, didFinish))
|
||||
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
self.event.updateDuration.emit(data: duration)
|
||||
self.delegate?.audioPlayer(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
self.event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: .playedUntilEnd)
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
self.event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension AudioPlayer {
|
||||
|
||||
@@ -15,6 +16,7 @@ extension AudioPlayer {
|
||||
public typealias FailEventData = (Error?)
|
||||
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = (Double)
|
||||
public typealias DidRecreateAVPlayerEventData = ()
|
||||
|
||||
public struct EventHolder {
|
||||
|
||||
@@ -37,7 +39,8 @@ extension AudioPlayer {
|
||||
public let secondElapse: AudioPlayer.Event<SecondElapseEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player encounters an error.
|
||||
Emitted when the player encounters an error. This will ultimately result in the AVPlayer instance to be recreated.
|
||||
If this event is emitted, it means you will need to load a new item in some way. Calling play() will not resume playback.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let fail: AudioPlayer.Event<FailEventData> = AudioPlayer.Event()
|
||||
@@ -54,13 +57,20 @@ extension AudioPlayer {
|
||||
*/
|
||||
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
- Note: It can be necessary to set the AVAudioSession's category again when this event is emitted.
|
||||
*/
|
||||
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
|
||||
|
||||
}
|
||||
|
||||
public typealias EventClosure<EventData> = (EventData) -> Void
|
||||
|
||||
class Invoker<EventData> {
|
||||
|
||||
// Signals false if the listener object is nil
|
||||
/// Signals false if the listener object is nil. If `false` is signaled, the invoker should not be retained.
|
||||
let invoke: (EventData) -> Bool
|
||||
weak var listener: AnyObject?
|
||||
|
||||
@@ -105,7 +115,7 @@ extension AudioPlayer {
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func emit(data: EventData) {
|
||||
eventQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
@@ -116,6 +126,34 @@ extension AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The publisher for this event. Use this for subscription through the Combine framework.
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
public lazy var publisher: EventPublisher<EventData> = EventPublisher(event: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension AudioPlayer {
|
||||
|
||||
public class EventPublisher<EventData>: Publisher {
|
||||
|
||||
public typealias Output = EventData
|
||||
public typealias Failure = APError.EventError
|
||||
|
||||
private weak var event: Event<EventData>?
|
||||
|
||||
init(event: Event<EventData>) {
|
||||
self.event = event
|
||||
}
|
||||
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
|
||||
event?.addListener(self) { (data) in
|
||||
let _ = subscriber.receive(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,39 +36,43 @@ class AVPlayerObserver: NSObject {
|
||||
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
|
||||
}
|
||||
|
||||
let player: AVPlayer
|
||||
|
||||
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
|
||||
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
|
||||
var isObserving: Bool = false
|
||||
|
||||
weak var delegate: AVPlayerObserverDelegate?
|
||||
|
||||
init(player: AVPlayer) {
|
||||
self.player = player
|
||||
weak var player: AVPlayer? {
|
||||
willSet {
|
||||
self.stopObserving()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if self.isObserving {
|
||||
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
|
||||
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
|
||||
}
|
||||
self.stopObserving()
|
||||
}
|
||||
|
||||
/**
|
||||
Start receiving events from this observer.
|
||||
|
||||
- Important: If this observer is already receiving events, it will first be removed. Never remove this observer manually.
|
||||
*/
|
||||
func startObserving() {
|
||||
main.async {
|
||||
if self.isObserving {
|
||||
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
|
||||
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
|
||||
}
|
||||
self.isObserving = true
|
||||
self.player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
self.player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
self.stopObserving()
|
||||
self.isObserving = true
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
}
|
||||
|
||||
func stopObserving() {
|
||||
guard let player = player, self.isObserving else {
|
||||
return
|
||||
}
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
|
||||
self.isObserving = false
|
||||
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
|
||||
@@ -27,7 +27,12 @@ class AVPlayerTimeObserver {
|
||||
var boundaryTimeStartObserverToken: Any?
|
||||
var periodicTimeObserverToken: Any?
|
||||
|
||||
private let player: AVPlayer
|
||||
weak var player: AVPlayer? {
|
||||
willSet {
|
||||
unregisterForBoundaryTimeEvents()
|
||||
unregisterForPeriodicEvents()
|
||||
}
|
||||
}
|
||||
|
||||
/// The frequence to receive periodic time events.
|
||||
/// Setting this to a new value will trigger a re-registering to the periodic events of the player.
|
||||
@@ -41,8 +46,7 @@ class AVPlayerTimeObserver {
|
||||
|
||||
weak var delegate: AVPlayerTimeObserverDelegate?
|
||||
|
||||
init(player: AVPlayer, periodicObserverTimeInterval: CMTime) {
|
||||
self.player = player
|
||||
init(periodicObserverTimeInterval: CMTime) {
|
||||
self.periodicObserverTimeInterval = periodicObserverTimeInterval
|
||||
}
|
||||
|
||||
@@ -50,9 +54,11 @@ class AVPlayerTimeObserver {
|
||||
Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.
|
||||
*/
|
||||
func registerForBoundaryTimeEvents() {
|
||||
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
unregisterForBoundaryTimeEvents()
|
||||
let startBoundaryTimes: [NSValue] = [AVPlayerTimeObserver.startBoundaryTime].map({NSValue(time: $0)})
|
||||
|
||||
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(forTimes: startBoundaryTimes, queue: nil, using: { [weak self] in
|
||||
self?.delegate?.audioDidStart()
|
||||
})
|
||||
@@ -62,10 +68,11 @@ class AVPlayerTimeObserver {
|
||||
Unregister from the boundary events of the player.
|
||||
*/
|
||||
func unregisterForBoundaryTimeEvents() {
|
||||
if let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken {
|
||||
player.removeTimeObserver(boundaryTimeStartObserverToken)
|
||||
self.boundaryTimeStartObserverToken = nil
|
||||
guard let player = player, let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken else {
|
||||
return
|
||||
}
|
||||
player.removeTimeObserver(boundaryTimeStartObserverToken)
|
||||
self.boundaryTimeStartObserverToken = nil
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +80,9 @@ class AVPlayerTimeObserver {
|
||||
Will trigger unregisterForPeriodicEvents() first to avoid multiple subscriptions.
|
||||
*/
|
||||
func registerForPeriodicTimeEvents() {
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
unregisterForPeriodicEvents()
|
||||
periodicTimeObserverToken = player.addPeriodicTimeObserver(forInterval: periodicObserverTimeInterval, queue: nil, using: { (time) in
|
||||
self.delegate?.timeEvent(time: time)
|
||||
@@ -83,12 +93,11 @@ class AVPlayerTimeObserver {
|
||||
Unregister for periodic events.
|
||||
*/
|
||||
func unregisterForPeriodicEvents() {
|
||||
if let periodicTimeObserverToken = periodicTimeObserverToken {
|
||||
self.player.removeTimeObserver(periodicTimeObserverToken)
|
||||
self.periodicTimeObserverToken = nil
|
||||
guard let player = player, let periodicTimeObserverToken = periodicTimeObserverToken else {
|
||||
return
|
||||
}
|
||||
player.removeTimeObserver(periodicTimeObserverToken)
|
||||
self.periodicTimeObserverToken = nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -120,7 +120,6 @@ public class QueuedAudioPlayer: AudioPlayer {
|
||||
*/
|
||||
public func next() throws {
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
delegate?.audioPlayer(itemPlaybackEndedWithReason: .skippedToNext)
|
||||
let nextItem = try queueManager.next()
|
||||
try self.load(item: nextItem, playWhenReady: true)
|
||||
}
|
||||
@@ -130,7 +129,6 @@ public class QueuedAudioPlayer: AudioPlayer {
|
||||
*/
|
||||
public func previous() throws {
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
delegate?.audioPlayer(itemPlaybackEndedWithReason: .skippedToPrevious)
|
||||
let previousItem = try queueManager.previous()
|
||||
try self.load(item: previousItem, playWhenReady: true)
|
||||
}
|
||||
@@ -154,7 +152,6 @@ public class QueuedAudioPlayer: AudioPlayer {
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
delegate?.audioPlayer(itemPlaybackEndedWithReason: .jumpedToIndex)
|
||||
let item = try queueManager.jump(to: index)
|
||||
try self.load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ public enum TimeEventFrequency {
|
||||
case everySecond
|
||||
case everyHalfSecond
|
||||
case everyQuarterSecond
|
||||
case custom(time: CMTime)
|
||||
|
||||
func getTime() -> CMTime {
|
||||
switch self {
|
||||
case .everySecond: return CMTime(value: 1, timescale: 1)
|
||||
case .everyHalfSecond: return CMTime(value: 1, timescale: 2)
|
||||
case .everyQuarterSecond: return CMTime(value: 1, timescale: 4)
|
||||
case .custom(let time): return time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||