Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e50ea48d6 | |||
| 92a804e9e4 | |||
| af4635d047 | |||
| addebef7f9 | |||
| 46022ef0d6 | |||
| 386dc5202c | |||
| 17166093c2 | |||
| c06b8ce64c | |||
| 5c8c83f914 | |||
| ef2d9f5a90 | |||
| 08ebb473f2 | |||
| d32a041159 | |||
| 1057a7fca6 | |||
| cee2be50e2 | |||
| 8161c3cf02 | |||
| bb0f301383 | |||
| 9da1347d12 | |||
| f8026d0915 | |||
| 9d6534f2c9 | |||
| c55c25686c | |||
| 852578c914 | |||
| 0ae0427556 | |||
| 9170be5e36 | |||
| 3930096357 | |||
| 9e8ebdfca1 | |||
| 31f4745bae | |||
| 4a8af1bce3 | |||
| 8e6b72613d | |||
| ae760c4cac | |||
| b3856e0a7a | |||
| 757e5f476c | |||
| 1f5eae407d | |||
| 29660371c9 | |||
| c2530a0193 | |||
| 6804cf5163 | |||
| 6ea2e082b0 | |||
| b6b4599ef5 | |||
| 3ccf9ed20e | |||
| d6eb187788 | |||
| 1febb782d8 | |||
| c4c6f42ac0 | |||
| d8b4466629 | |||
| cb9dec49b2 | |||
| 7e46a91e73 | |||
| 8ca4a873a5 | |||
| 3f54de70bc | |||
| cae549a401 | |||
| 85fecef45e | |||
| e2375f7a82 | |||
| 3c8402503b | |||
| 4c46137498 | |||
| a4e023d75f | |||
| fe549a522a | |||
| 6cc0638a70 | |||
| 34c1f493ae | |||
| 30750a0c81 | |||
| e57cf9d2e5 | |||
| 0eeedc6467 | |||
| e96cbf6337 | |||
| debc1c519f | |||
| 09bec023a9 | |||
| 44e022389c | |||
| 05ca97b8eb | |||
| f6ff2b4cc0 | |||
| 9e3f5c0291 | |||
| 8ba40ca45f | |||
| 9919bde9fe | |||
| 3ba62d8657 | |||
| 0f283b171f | |||
| f6b5e30e85 | |||
| 04296fa681 | |||
| 252ed947d2 | |||
| b5fdb5c54e | |||
| 1b4c0b0d3b | |||
| e0aa2a09a9 | |||
| ab0eb4f8eb | |||
| 99e7c65bbc | |||
| 9072259631 | |||
| eb9af1007a | |||
| 69d3a9c0c0 | |||
| 43821c68a9 | |||
| 610ff4c7f3 | |||
| 1fc533214f | |||
| 71f22c3e25 | |||
| 8a5e6d18cc | |||
| a7f53bfec9 | |||
| 7b10f0476b | |||
| 618da75339 | |||
| 0694f5d8a0 | |||
| 61268b45eb | |||
| 962e64fe24 | |||
| 0f9b2656a1 | |||
| 5e871fc7e2 | |||
| fdf8fc9482 | |||
| a9eb713964 | |||
| a0efa5f408 | |||
| 74cafd4c42 | |||
| 53780ac03e | |||
| ba438c8ede |
@@ -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.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.0
|
||||
4.2
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
# references:
|
||||
# * http://www.objc.io/issue-6/travis-ci.html
|
||||
# * https://github.com/supermarin/xcpretty#usage
|
||||
|
||||
osx_image: xcode9.4
|
||||
language: swift
|
||||
cache: cocoapods
|
||||
podfile: Example/Podfile
|
||||
before_install:
|
||||
- gem install cocoapods # Since Travis is not always on latest version
|
||||
- pod install --project-directory=Example --repo-update
|
||||
script:
|
||||
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftAudio.xcworkspace -scheme SwiftAudio-Example -sdk iphonesimulator11.4 -destination "OS=11.4,name=iPhone X" | xcpretty
|
||||
- pod lib lint
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash) -J 'SwiftAudio'
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
Generated
+9
-9
@@ -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
|
||||
|
||||
Generated
+7
-22
@@ -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
|
||||
|
||||
+10
-4
@@ -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
|
||||
|
||||
+11
-7
@@ -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
|
||||
}
|
||||
|
||||
+16
-13
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+10
-4
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+8
-1
@@ -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)
|
||||
}
|
||||
|
||||
+25
-4
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-30
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-18
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-118
@@ -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
|
||||
|
||||
+18
-9
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
+3
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+13
-19
@@ -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() {
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+3
-2
@@ -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)
|
||||
|
||||
+997
-930
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1AB61EB02FDF0033DCB1F8416419F110"
|
||||
BuildableName = "SwiftAudio.framework"
|
||||
BlueprintName = "SwiftAudio"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1AB61EB02FDF0033DCB1F8416419F110"
|
||||
BuildableName = "SwiftAudio.framework"
|
||||
BlueprintName = "SwiftAudio"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
Generated
+3
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
+3
-9
@@ -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)
|
||||
|
||||
+3
-3
@@ -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
-1
@@ -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
-9
@@ -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
|
||||
|
||||
+3
-9
@@ -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
|
||||
|
||||
+3
-9
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#if canImport(Darwin)
|
||||
|
||||
import XCTest
|
||||
|
||||
|
||||
+5
-11
@@ -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)
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
|
||||
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
|
||||
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
|
||||
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */; };
|
||||
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */; };
|
||||
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */; };
|
||||
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */; };
|
||||
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
|
||||
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
|
||||
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
|
||||
@@ -32,6 +36,7 @@
|
||||
07756B69218A4E870023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B68218A4E870023935E /* AudioSession.swift */; };
|
||||
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */; };
|
||||
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */; };
|
||||
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */; };
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
@@ -65,12 +70,17 @@
|
||||
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
|
||||
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
|
||||
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
|
||||
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoControllerTests.swift; sourceTree = "<group>"; };
|
||||
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoCenter.swift; sourceTree = "<group>"; };
|
||||
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoController.swift; sourceTree = "<group>"; };
|
||||
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerEventTests.swift; sourceTree = "<group>"; };
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
|
||||
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
|
||||
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
|
||||
07756B68218A4E870023935E /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = "<group>"; };
|
||||
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemObserverTests.swift; sourceTree = "<group>"; };
|
||||
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueuedAudioPlayerTests.swift; sourceTree = "<group>"; };
|
||||
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoTests.swift; sourceTree = "<group>"; };
|
||||
521F3AEC1228A2FA2637355F /* Pods-SwiftAudio_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudio_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudio_Tests/Pods-SwiftAudio_Tests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -128,6 +138,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
07756B68218A4E870023935E /* AudioSession.swift */,
|
||||
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */,
|
||||
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
@@ -193,6 +205,9 @@
|
||||
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
|
||||
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
|
||||
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
|
||||
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */,
|
||||
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */,
|
||||
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */,
|
||||
0708ED712116E91300EB29BD /* Source */,
|
||||
607FACE91AFB9204008FA782 /* Supporting Files */,
|
||||
);
|
||||
@@ -286,13 +301,13 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 0830;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = CocoaPods;
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 1;
|
||||
@@ -302,14 +317,14 @@
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1020;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudio" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
@@ -452,16 +467,21 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
07756B69218A4E870023935E /* AudioSession.swift in Sources */,
|
||||
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */,
|
||||
0708ED702116E89900EB29BD /* Source.swift in Sources */,
|
||||
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */,
|
||||
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
|
||||
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
|
||||
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
|
||||
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */,
|
||||
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */,
|
||||
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */,
|
||||
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */,
|
||||
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */,
|
||||
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
|
||||
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
|
||||
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */,
|
||||
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -507,12 +527,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -560,12 +582,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -606,8 +630,7 @@
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -624,8 +647,7 @@
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
@@ -647,8 +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_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -666,8 +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_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0900"
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
|
||||
application.beginReceivingRemoteControlEvents()
|
||||
|
||||
@@ -17,10 +17,10 @@ class AudioController {
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
]
|
||||
|
||||
init() {
|
||||
|
||||
@@ -46,7 +46,7 @@ extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
case 0:
|
||||
return 1
|
||||
case 1:
|
||||
return controller.player.nextItems.count ?? 0
|
||||
return controller.player.nextItems.count
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -27,14 +27,18 @@ class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
controller.player.delegate = self
|
||||
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
|
||||
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) {
|
||||
if (!controller.audioSessionController.audioSessionIsActive) {
|
||||
try? controller.audioSessionController.activateSession()
|
||||
}
|
||||
try? controller.player.togglePlaying()
|
||||
controller.player.togglePlaying()
|
||||
}
|
||||
|
||||
@IBAction func previous(_ sender: Any) {
|
||||
@@ -50,7 +54,7 @@ class ViewController: UIViewController {
|
||||
}
|
||||
|
||||
@IBAction func scrubbing(_ sender: UISlider) {
|
||||
try? controller.player.seek(to: Double(slider.value))
|
||||
controller.player.seek(to: Double(slider.value))
|
||||
}
|
||||
|
||||
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
|
||||
@@ -58,63 +62,63 @@ class ViewController: UIViewController {
|
||||
elapsedTimeLabel.text = value.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController: AudioPlayerDelegate {
|
||||
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
|
||||
|
||||
|
||||
func updateTimeValues() {
|
||||
self.slider.maximumValue = Float(self.controller.player.duration)
|
||||
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
|
||||
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
|
||||
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
|
||||
}
|
||||
|
||||
func updateMetaData() {
|
||||
if let item = controller.player.currentItem {
|
||||
titleLabel.text = item.getTitle()
|
||||
artistLabel.text = item.getArtist()
|
||||
item.getArtwork({ (image) in
|
||||
self.imageView.image = image
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
|
||||
func setPlayButtonState(forAudioPlayerState state: AudioPlayerState) {
|
||||
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
|
||||
|
||||
switch state {
|
||||
case .ready:
|
||||
|
||||
if let item = controller.player.currentItem {
|
||||
titleLabel.text = item.getTitle()
|
||||
artistLabel.text = item.getArtist()
|
||||
item.getArtwork({ (image) in
|
||||
self.imageView.image = image
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - AudioPlayer Event Handlers
|
||||
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
DispatchQueue.main.async {
|
||||
self.setPlayButtonState(forAudioPlayerState: data)
|
||||
switch data {
|
||||
case .ready:
|
||||
self.updateMetaData()
|
||||
self.updateTimeValues()
|
||||
case .loading, .playing, .paused, .idle:
|
||||
self.updateTimeValues()
|
||||
}
|
||||
|
||||
slider.maximumValue = Float(controller.player.duration)
|
||||
slider.setValue(Float(controller.player.currentTime), animated: true)
|
||||
|
||||
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
|
||||
|
||||
case .loading, .playing, .paused, .idle:
|
||||
slider.maximumValue = Float(controller.player.duration)
|
||||
slider.setValue(Float(controller.player.currentTime), animated: true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func audioPlayer(secondsElapsed seconds: Double) {
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
slider.setValue(Float(seconds), animated: false)
|
||||
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func audioPlayer(failedWithError error: Error?) {
|
||||
|
||||
}
|
||||
|
||||
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
|
||||
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
|
||||
isScrubbing = false
|
||||
}
|
||||
|
||||
func audioPlayer(didUpdateDuration duration: Double) {
|
||||
slider.maximumValue = Float(controller.player.duration)
|
||||
slider.setValue(Float(controller.player.currentTime), animated: true)
|
||||
|
||||
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
|
||||
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
try? controller.audioSessionController.set(category: .playback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import AVFoundation
|
||||
|
||||
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
|
||||
|
||||
var status: AVPlayerStatus?
|
||||
var timeControlStatus: AVPlayerTimeControlStatus?
|
||||
var status: AVPlayer.Status?
|
||||
var timeControlStatus: AVPlayer.TimeControlStatus?
|
||||
|
||||
override func spec() {
|
||||
|
||||
@@ -19,10 +19,16 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
|
||||
|
||||
beforeEach {
|
||||
player = AVPlayer()
|
||||
observer = AVPlayerObserver(player: player)
|
||||
player.volume = 0.0
|
||||
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()
|
||||
@@ -53,16 +59,27 @@ 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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayerStatus) {
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
self.timeControlStatus = status
|
||||
}
|
||||
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -14,10 +14,9 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
var wrapper: AVPlayerWrapper!
|
||||
|
||||
beforeEach {
|
||||
let player = AVPlayer()
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
player.volume = 0.0
|
||||
wrapper = AVPlayerWrapper(avPlayer: player)
|
||||
wrapper = AVPlayerWrapper()
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = false
|
||||
wrapper.volume = 0.0
|
||||
wrapper.bufferDuration = 0.0001
|
||||
}
|
||||
|
||||
@@ -30,10 +29,6 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should be loading", closure: {
|
||||
expect(wrapper.state).to(equal(AVPlayerWrapperState.loading))
|
||||
})
|
||||
|
||||
it("should eventually be ready", closure: {
|
||||
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
|
||||
@@ -128,6 +123,17 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
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", {
|
||||
@@ -137,7 +143,7 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
|
||||
context("when loading source", {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
|
||||
wrapper.load(from: URL(fileURLWithPath: LongSource.path), playWhenReady: false)
|
||||
}
|
||||
it("should eventually not be 0", closure: {
|
||||
expect(wrapper.duration).toEventuallyNot(equal(0))
|
||||
@@ -151,18 +157,27 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when seeking to a time", {
|
||||
var passed = false
|
||||
let holder = AVPlayerWrapperDelegateHolder()
|
||||
let seekTime: TimeInterval = 0.5
|
||||
beforeEach {
|
||||
wrapper.delegate = holder
|
||||
holder.seekCompletion = { passed = true }
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
}
|
||||
|
||||
it("should eventually be equal to the seeked time", closure: {
|
||||
expect(passed).toEventually(beTrue())
|
||||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -219,7 +234,11 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
class AudioPlayerEventTests: QuickSpec {
|
||||
|
||||
class EventListener {
|
||||
var handleEvent: ((Void)) -> Void = { _ in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An event") {
|
||||
var event: AudioPlayer.Event<(Void)>!
|
||||
beforeEach {
|
||||
event = AudioPlayer.Event()
|
||||
}
|
||||
|
||||
describe("its invokers", {
|
||||
|
||||
context("when adding a listener", {
|
||||
var listener: EventListener!
|
||||
beforeEach {
|
||||
listener = EventListener()
|
||||
event.addListener(listener, listener!.handleEvent)
|
||||
}
|
||||
|
||||
it("should have one element", closure: {
|
||||
expect(event.invokers.count).toEventuallyNot(equal(0))
|
||||
})
|
||||
|
||||
context("then that listener is deinitialized and an an event is emitted", {
|
||||
beforeEach {
|
||||
listener = nil
|
||||
event.emit(data: ())
|
||||
}
|
||||
|
||||
it("should remove the invoker", closure: {
|
||||
expect(event.invokers.count).toEventually(equal(0))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
context("when adding multiple listeners", {
|
||||
var listeners: [EventListener]!
|
||||
|
||||
beforeEach {
|
||||
listeners = [0..<15].map {_ in
|
||||
let listener = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
return listener
|
||||
}
|
||||
}
|
||||
|
||||
it("should have several listeners", closure: {
|
||||
expect(event.invokers.count).toEventually(equal(listeners.count))
|
||||
})
|
||||
|
||||
context("then removing one", {
|
||||
beforeEach {
|
||||
event.removeListener(listeners[listeners.count / 2])
|
||||
}
|
||||
|
||||
it("should have one less invoker", closure: {
|
||||
expect(event.invokers.count).toEventually(equal(listeners.count - 1))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
@@ -43,14 +44,12 @@ class AudioPlayerTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when playing an item", {
|
||||
var holder: AudioPlayerDelegateHolder!
|
||||
var listener: AudioPlayerEventListener!
|
||||
beforeEach {
|
||||
holder = AudioPlayerDelegateHolder()
|
||||
audioPlayer.delegate = holder
|
||||
holder.stateUpdate = { state in
|
||||
print(state.rawValue)
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
listener.stateUpdate = { state in
|
||||
if state == .ready {
|
||||
try? audioPlayer.play()
|
||||
audioPlayer.play()
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
@@ -62,13 +61,12 @@ class AudioPlayerTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when pausing an item", {
|
||||
var holder: AudioPlayerDelegateHolder!
|
||||
var listener: AudioPlayerEventListener!
|
||||
beforeEach {
|
||||
holder = AudioPlayerDelegateHolder()
|
||||
audioPlayer.delegate = holder
|
||||
holder.stateUpdate = { (state) in
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
listener.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
try? audioPlayer.pause()
|
||||
audioPlayer.pause()
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
@@ -80,11 +78,10 @@ class AudioPlayerTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when stopping an item", {
|
||||
var holder: AudioPlayerDelegateHolder!
|
||||
var listener: AudioPlayerEventListener!
|
||||
beforeEach {
|
||||
holder = AudioPlayerDelegateHolder()
|
||||
audioPlayer.delegate = holder
|
||||
holder.stateUpdate = { (state) in
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
listener.stateUpdate = { (state) in
|
||||
if state == .playing {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
@@ -105,18 +102,26 @@ class AudioPlayerTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when seeking to a time", {
|
||||
var passed = false
|
||||
let holder = AudioPlayerDelegateHolder()
|
||||
let seekTime: TimeInterval = 0.5
|
||||
let seekTime: TimeInterval = 1.0
|
||||
beforeEach {
|
||||
audioPlayer.delegate = holder
|
||||
holder.seekCompletion = { passed = true }
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: seekTime)
|
||||
}
|
||||
|
||||
it("should eventually be equal to the seeked time", closure: {
|
||||
expect(passed).toEventually(beTrue())
|
||||
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()))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -151,19 +156,42 @@ class AudioPlayerTests: QuickSpec {
|
||||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AudioPlayerDelegateHolder: AudioPlayerDelegate {
|
||||
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
|
||||
|
||||
}
|
||||
class AudioPlayerEventListener {
|
||||
|
||||
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var state: AudioPlayerState? {
|
||||
didSet {
|
||||
if let state = state {
|
||||
@@ -172,29 +200,20 @@ class AudioPlayerDelegateHolder: AudioPlayerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func audioPlayer(playerDidChangeState state: AudioPlayerState) {
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var seekCompletion: (() -> Void)?
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
|
||||
audioPlayer.event.seek.addListener(self, handleSeek)
|
||||
}
|
||||
|
||||
func handleDidUpdateState(state: AudioPlayerState) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
func audioPlayer(secondsElapsed seconds: Double) {
|
||||
|
||||
}
|
||||
|
||||
func audioPlayer(failedWithError error: Error?) {
|
||||
|
||||
}
|
||||
|
||||
var seekCompletion: (() -> Void)?
|
||||
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
|
||||
func handleSeek(data: AudioPlayer.SeekEventData) {
|
||||
seekCompletion?()
|
||||
}
|
||||
|
||||
func audioPlayer(didUpdateDuration duration: Double) {
|
||||
if let state = self.state {
|
||||
self.stateUpdate?(state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
context("when a interruption arrives", {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: .AVAudioSessionInterruption, object: nil, userInfo: [
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0)
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
@@ -92,9 +92,9 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
|
||||
var interruptionType: AVAudioSessionInterruptionType? = nil
|
||||
var interruptionType: AVAudioSession.InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: AVAudioSessionInterruptionType) {
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,31 +14,43 @@ import AVFoundation
|
||||
|
||||
class NonFailingAudioSession: AudioSession {
|
||||
|
||||
var category: AVAudioSession.Category = AVAudioSession.Category.playback
|
||||
|
||||
var mode: AVAudioSession.Mode = AVAudioSession.Mode.default
|
||||
|
||||
var categoryOptions: AVAudioSession.CategoryOptions = []
|
||||
|
||||
var availableCategories: [AVAudioSession.Category] = []
|
||||
|
||||
var isOtherAudioPlaying: Bool = false
|
||||
|
||||
var availableCategories: [String] = []
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {}
|
||||
|
||||
func setCategory(_ category: String) throws {}
|
||||
|
||||
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {}
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {}
|
||||
|
||||
func setActive(_ active: Bool) throws {}
|
||||
|
||||
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {}
|
||||
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {}
|
||||
|
||||
}
|
||||
|
||||
class FailingAudioSession: AudioSession {
|
||||
|
||||
var category: AVAudioSession.Category = AVAudioSession.Category.playback
|
||||
|
||||
var mode: AVAudioSession.Mode = AVAudioSession.Mode.default
|
||||
|
||||
var categoryOptions: AVAudioSession.CategoryOptions = AVAudioSession.CategoryOptions.allowBluetooth
|
||||
|
||||
var availableCategories: [AVAudioSession.Category] = []
|
||||
|
||||
var isOtherAudioPlaying: Bool = false
|
||||
|
||||
var availableCategories: [String] = []
|
||||
|
||||
func setCategory(_ category: String) throws {
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
@@ -46,7 +58,7 @@ class FailingAudioSession: AudioSession {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {
|
||||
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// NowPlayingInfoCenter.swift
|
||||
// SwiftAudio_Tests
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 03/03/2019.
|
||||
// Copyright © 2019 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
|
||||
|
||||
var nowPlayingInfo: [String : Any]? = nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// NowPlayingInfoController.swift
|
||||
// SwiftAudio_Tests
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 03/03/2019.
|
||||
// Copyright © 2019 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
|
||||
|
||||
var info: [String: Any] = [:]
|
||||
|
||||
required public init() {
|
||||
}
|
||||
|
||||
required public init(infoCenter: NowPlayingInfoCenter) {
|
||||
}
|
||||
|
||||
public func set(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
keyValues.forEach { (keyValue) in
|
||||
info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
}
|
||||
|
||||
public func set(keyValue: NowPlayingInfoKeyValue) {
|
||||
info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
|
||||
func getTitle() -> String? {
|
||||
return info[MediaItemProperty.title(nil).getKey()] as? String
|
||||
}
|
||||
|
||||
func getArtist() -> String? {
|
||||
return info[MediaItemProperty.artist(nil).getKey()] as? String
|
||||
}
|
||||
|
||||
func getAlbumTitle() -> String? {
|
||||
return info[MediaItemProperty.albumTitle(nil).getKey()] as? String
|
||||
}
|
||||
|
||||
func getRate() -> Double? {
|
||||
return info[NowPlayingInfoProperty.playbackRate(nil).getKey()] as? Double
|
||||
}
|
||||
|
||||
func getDuration() -> Double? {
|
||||
return info[MediaItemProperty.duration(nil).getKey()] as? Double
|
||||
}
|
||||
|
||||
func getCurrentTime() -> Double? {
|
||||
return info[NowPlayingInfoProperty.elapsedPlaybackTime(nil).getKey()] as? Double
|
||||
}
|
||||
|
||||
func getArtwork() -> MPMediaItemArtwork? {
|
||||
return info[MediaItemProperty.artwork(nil).getKey()] as? MPMediaItemArtwork
|
||||
}
|
||||
|
||||
func clear() {
|
||||
info = [:]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
class NowPlayingInfoControllerTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
describe("An NowPlayingInfoController") {
|
||||
|
||||
var nowPlayingController: NowPlayingInfoController!
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController = NowPlayingInfoController(infoCenter: NowPlayingInfoCenter_Mock())
|
||||
}
|
||||
|
||||
describe("its info dictionary") {
|
||||
|
||||
context("when setting a value") {
|
||||
beforeEach {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
}
|
||||
|
||||
it("should not be empty") {
|
||||
expect(nowPlayingController.info.count).toNot(equal(0))
|
||||
}
|
||||
|
||||
context("then calling clear()") {
|
||||
beforeEach {
|
||||
nowPlayingController.clear()
|
||||
}
|
||||
|
||||
it("should be empty", closure: {
|
||||
expect(nowPlayingController.info.count).to(equal(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its info center") {
|
||||
|
||||
context("when setting a value") {
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo).toNot(beNil())
|
||||
}
|
||||
|
||||
it("should not be empty") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).toNot(equal(0))
|
||||
}
|
||||
|
||||
context("then calling clear()") {
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController.clear()
|
||||
}
|
||||
|
||||
it("should be empty", closure: {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(equal(0))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
|
||||
class NowPlayingInfoTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An AudioPlayer") {
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var nowPlayingController: NowPlayingInfoController_Mock!
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController = NowPlayingInfoController_Mock()
|
||||
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
|
||||
audioPlayer.automaticallyUpdateNowPlayingInfo = true
|
||||
audioPlayer.volume = 0
|
||||
}
|
||||
|
||||
describe("its NowPlayingInfoController", {
|
||||
|
||||
context("when loading an AudioItem", {
|
||||
|
||||
var item: AudioItem!
|
||||
|
||||
beforeEach {
|
||||
item = Source.getAudioItem()
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should eventually be updated with meta data", closure: {
|
||||
expect(nowPlayingController.getTitle()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getTitle()).toEventually(equal(item.getTitle()!))
|
||||
|
||||
expect(nowPlayingController.getArtist()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getArtist()).toEventually(equal(item.getArtist()!))
|
||||
|
||||
expect(nowPlayingController.getAlbumTitle()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getAlbumTitle()).toEventually(equal(item.getAlbumTitle()!))
|
||||
|
||||
expect(nowPlayingController.getArtwork()).toEventuallyNot(beNil())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
context("when playing an AudioItem", {
|
||||
|
||||
var item: AudioItem!
|
||||
|
||||
beforeEach {
|
||||
item = LongSource.getAudioItem()
|
||||
try? audioPlayer.load(item: item, playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be updated with playback values", closure: {
|
||||
expect(nowPlayingController.getRate()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getDuration()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getCurrentTime()).toEventuallyNot(beNil())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class QueueManagerTests: QuickSpec {
|
||||
|
||||
context("then replacing the item", closure: {
|
||||
beforeEach {
|
||||
try? manager.replaceCurrentItem(with: 1)
|
||||
manager.replaceCurrentItem(with: 1)
|
||||
}
|
||||
it("should have replaced the current item", closure: {
|
||||
expect(manager.current).to(equal(1))
|
||||
@@ -170,6 +170,18 @@ class QueueManagerTests: QuickSpec {
|
||||
expect(manager.current).to(equal(self.dummyItems.first))
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing previous items", {
|
||||
beforeEach {
|
||||
manager.removePreviousItems()
|
||||
}
|
||||
it("should have no previous items", closure: {
|
||||
expect(manager.previousItems.count).to(equal(0))
|
||||
})
|
||||
it("should have current index zero", closure: {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context("adding more items", {
|
||||
@@ -205,7 +217,7 @@ class QueueManagerTests: QuickSpec {
|
||||
var removed: Int?
|
||||
var initialCurrentIndex: Int!
|
||||
beforeEach {
|
||||
try? manager.jump(to: 3)
|
||||
let _ = try? manager.jump(to: 3)
|
||||
initialCurrentIndex = manager.currentIndex
|
||||
removed = try? manager.removeItem(at: initialCurrentIndex - 1)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,26 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing upcoming items", {
|
||||
beforeEach {
|
||||
audioPlayer.removeUpcomingItems()
|
||||
}
|
||||
|
||||
it("should be empty", closure: {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
context("then stopping", {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
it("should be empty", closure: {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,6 +144,26 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing all previous items", {
|
||||
beforeEach {
|
||||
audioPlayer.removePreviousItems()
|
||||
}
|
||||
|
||||
it("should be empty", closure: {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
context("then stopping", {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
it("should be empty", closure: {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,15 +14,24 @@ struct Source {
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
return DefaultAudioItem(audioUrl: Source.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
|
||||
}
|
||||
}
|
||||
|
||||
struct ShortSource {
|
||||
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
static let url: URL = URL(fileURLWithPath: ShortSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
struct LongSource {
|
||||
static let path: String = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
|
||||
static let url: URL = URL(fileURLWithPath: LongSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: LongSource.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SwiftAudio
|
||||
|
||||
[](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
|
||||
[](https://app.bitrise.io/app/3d3ac2ba8d817235)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
@@ -18,30 +18,54 @@ iOS 10.0+
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods
|
||||
SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'SwiftAudio', '~> 0.4.0'
|
||||
pod 'SwiftAudio', '~> 0.9.0'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
|
||||
```ruby
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.0
|
||||
```
|
||||
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
|
||||
|
||||
## Usage
|
||||
|
||||
### AudioPlayer
|
||||
To get started playing some audio:
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
player.load(item: audioItem, playWhenReady: true) // Load the item and start playing when the player is ready.
|
||||
```
|
||||
|
||||
Implement `AudioPlayerDelegate` to get notified about useful events and updates to the state of the `AudioPlayer`.
|
||||
To listen for events in the `AudioPlayer`, subscribe to events found in the `event` property of the `AudioPlayer`.
|
||||
To subscribe to an event:
|
||||
```swift
|
||||
class MyCustomViewController: UIViewController {
|
||||
|
||||
let audioPlayer = AudioPlayer()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
audioPlayer.event.stateChange.addListener(self, handleAudioPlayerStateChange)
|
||||
}
|
||||
|
||||
func handleAudioPlayerStateChange(state: AudioPlayerState) {
|
||||
// Handle the event
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### QueuedAudioPlayer
|
||||
The `QueuedAudioPlayer` is asubclass of `AudioPlayer` that maintains a queue of audio tracks.
|
||||
The `QueuedAudioPlayer` is a subclass of `AudioPlayer` that maintains a queue of audio tracks.
|
||||
```swift
|
||||
let player = QueuedAudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
player.add(item: audioItem, playWhenReady: true) // Since this is the first item, we can supply playWhenReady: true to immedietaly start playing when the item is loaded.
|
||||
```
|
||||
|
||||
@@ -69,15 +93,16 @@ Current options for configuring the `AudioPlayer`:
|
||||
- `volume`
|
||||
- `isMuted`
|
||||
- `rate`
|
||||
- `audioTimePitchAlgorithm`: This value decides the `AVAudioTimePitchAlgorithm` used for each `AudioItem`. Implement `TimePitching` in your `AudioItem`-subclass to override individually for each `AudioItem`.
|
||||
|
||||
### Audio Session
|
||||
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionController`:
|
||||
```swift
|
||||
try? AudioSessionController.set(category: .playback)
|
||||
try? AudioSessionController.shared.set(category: .playback)
|
||||
//...
|
||||
// You should wait with activating the session until you actually start playback of audio.
|
||||
// This is to avoid interrupting other audio without the need to do it.
|
||||
try? AudioSessionController.activateSession()
|
||||
try? AudioSessionController.shared.activateSession()
|
||||
```
|
||||
|
||||
**Important**: If you want audio to continue playing when the app is inactive, remember to activate background audio:
|
||||
@@ -90,8 +115,25 @@ If you are storing progress for playback time on items when the app quits, it ca
|
||||
To disable interruption notifcations set `isObservingForInterruptions` to `false`.
|
||||
|
||||
### Now Playing Info
|
||||
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork and time if the passed in `AudioItem` supports this. This functionality can be turned off by setting `automaticallyUpdateNowPlayingInfo` to `false`.
|
||||
If you need to set additional properties for some items, access the player's `NowPlayingInfoController` and call `set(keyValue:)`. Available properties can be found in `NowPlayingInfoProperty`.
|
||||
The `AudioPlayer` can automatically update `nowPlayingInfo` for you. This requires `automaticallyUpdateNowPlayingInfo` to be true (default), and that the `AudioItem` that is passed in return values for the getters. The `AudioPlayer` will update: artist, title, album, artwork, elapsed time, duration and rate.
|
||||
|
||||
Additional properties for items can be set by accessing the setter of the `nowPlayingInforController`:
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.load(item: someItem)
|
||||
player.nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.isLiveStream(true))
|
||||
```
|
||||
The set(keyValue:) and set(keyValues:) accept both `MediaItemProperty` and `NowPlayingInfoProperty`.
|
||||
|
||||
The info can be forced to reload/update from the `AudioPlayer`.
|
||||
```swift
|
||||
audioPlayer.loadNowPlayingMetaValues()
|
||||
audioPlayer.updateNowPlayingPlaybackValues()
|
||||
```
|
||||
The current info can be cleared with:
|
||||
```swift
|
||||
audioPlayer.nowPlayingInfoController.clear()
|
||||
```
|
||||
|
||||
### Remote Commands
|
||||
**First** go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
|
||||
@@ -117,6 +159,9 @@ player.remoteCommandController.handlePlayCommand = { (event) in
|
||||
```
|
||||
All available overrides can be found by looking at `RemoteCommandController`.
|
||||
|
||||
### Start playback from a certain point in time
|
||||
Make your `AudioItem`-subclass conform to `InitialTiming` to be able to start playback from a certain time.
|
||||
|
||||
## Author
|
||||
|
||||
Jørgen Henrichsen
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.4.0'
|
||||
s.version = '0.9.0'
|
||||
s.summary = 'Easy audio streaming for iOS'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -26,7 +26,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let avPlayer: AVPlayer
|
||||
var avPlayer: AVPlayer
|
||||
let playerObserver: AVPlayerObserver
|
||||
let playerTimeObserver: AVPlayerTimeObserver
|
||||
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
||||
@@ -36,6 +36,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
fileprivate var _playWhenReady: Bool = true
|
||||
fileprivate var _initialTime: TimeInterval?
|
||||
|
||||
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
@@ -45,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()
|
||||
|
||||
@@ -85,12 +88,23 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
var duration: TimeInterval {
|
||||
if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0
|
||||
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
@@ -130,6 +144,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
pause()
|
||||
case .paused:
|
||||
play()
|
||||
@unknown default:
|
||||
fatalError("Unknown AVPlayer.timeControlStatus")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,15 +155,24 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, 1)) { (finished) in
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
||||
if let _ = self._initialTime {
|
||||
self._initialTime = nil
|
||||
if self._playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
|
||||
public func load(from url: URL, playWhenReady: Bool) {
|
||||
func load(from url: URL, playWhenReady: Bool) {
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
_state = .loading
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
recreateAVPlayer()
|
||||
}
|
||||
|
||||
// Set item
|
||||
let currentAsset = AVURLAsset(url: url)
|
||||
@@ -162,15 +187,32 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
playerItemObserver.startObserving(item: currentItem)
|
||||
}
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
|
||||
_initialTime = initialTime
|
||||
self.pause()
|
||||
self.load(from: url, playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
// MARK: - Util
|
||||
|
||||
private func reset(soft: Bool) {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -179,7 +221,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
switch status {
|
||||
case .paused:
|
||||
if currentItem == nil {
|
||||
@@ -192,17 +234,24 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
self._state = .loading
|
||||
case .playing:
|
||||
self._state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayerStatus) {
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
|
||||
|
||||
case .readyToPlay:
|
||||
self._state = .ready
|
||||
if _playWhenReady {
|
||||
|
||||
if let initialTime = _initialTime {
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
else if _playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case .failed:
|
||||
@@ -211,6 +260,8 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
|
||||
case .unknown:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +286,7 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
// MARK: - AVPlayerItemNotificationObserverDelegate
|
||||
|
||||
func itemDidPlayToEndTime() {
|
||||
delegate?.AVWrapper(itemPlaybackDoneWithReason: .playedUntilEnd)
|
||||
delegate?.AVWrapperItemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ import Foundation
|
||||
protocol AVPlayerWrapperDelegate: class {
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState)
|
||||
func AVWrapper(itemPlaybackDoneWithReason: PlaybackEndedReason)
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
}
|
||||
|
||||
Regular → Executable
+6
-2
@@ -19,10 +19,12 @@ protocol AVPlayerWrapperProtocol {
|
||||
|
||||
var duration: TimeInterval { get }
|
||||
|
||||
var bufferedPosition: TimeInterval { get }
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
var delegate: AVPlayerWrapperDelegate? { get set }
|
||||
|
||||
@@ -35,7 +37,7 @@ protocol AVPlayerWrapperProtocol {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
@@ -49,4 +51,6 @@ protocol AVPlayerWrapperProtocol {
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
|
||||
|
||||
}
|
||||
|
||||
@@ -20,14 +20,24 @@ public protocol AudioItem {
|
||||
func getTitle() -> String?
|
||||
func getAlbumTitle() -> String?
|
||||
func getSourceType() -> SourceType
|
||||
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
|
||||
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
|
||||
|
||||
}
|
||||
|
||||
public struct DefaultAudioItem: AudioItem {
|
||||
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
|
||||
public protocol TimePitching {
|
||||
|
||||
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
|
||||
|
||||
}
|
||||
|
||||
/// Make your `AudioItem`-subclass conform to this protocol to control enable the ability to start an item at a specific time of playback.
|
||||
public protocol InitialTiming {
|
||||
func getInitialTime() -> TimeInterval
|
||||
}
|
||||
|
||||
public class DefaultAudioItem: AudioItem {
|
||||
|
||||
public var audioUrl: String
|
||||
|
||||
public var artist: String?
|
||||
@@ -38,17 +48,14 @@ public struct DefaultAudioItem: AudioItem {
|
||||
|
||||
public var sourceType: SourceType
|
||||
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public var artwork: UIImage?
|
||||
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, pitchAlgorithmType: AVAudioTimePitchAlgorithm, artwork: UIImage? = nil) {
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
|
||||
self.audioUrl = audioUrl
|
||||
self.artist = artist
|
||||
self.title = title
|
||||
self.albumTitle = albumTitle
|
||||
self.sourceType = sourceType
|
||||
self.pitchAlgorithmType = pitchAlgorithmType
|
||||
self.artwork = artwork
|
||||
}
|
||||
|
||||
@@ -71,13 +78,50 @@ public struct DefaultAudioItem: AudioItem {
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
}
|
||||
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
handler(artwork)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// An AudioItem that also conforms to the `TimePitching`-protocol
|
||||
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
|
||||
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
|
||||
self.pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
}
|
||||
}
|
||||
|
||||
/// An AudioItem that also conforms to the `InitialTiming`-protocol
|
||||
public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
|
||||
public var initialTime: TimeInterval
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.initialTime = 0.0
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, initialTime: TimeInterval) {
|
||||
self.initialTime = initialTime
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public func getInitialTime() -> TimeInterval {
|
||||
return initialTime
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,29 +10,18 @@ import MediaPlayer
|
||||
|
||||
public typealias AudioPlayerState = AVPlayerWrapperState
|
||||
|
||||
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
|
||||
private var _wrapper: AVPlayerWrapperProtocol
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoController
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
var wrapper: AVPlayerWrapperProtocol {
|
||||
return _wrapper
|
||||
}
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
public let event = EventHolder()
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
public var currentItem: AudioItem? {
|
||||
@@ -44,6 +33,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
/**
|
||||
Controls the time pitch algorithm applied to each item loaded into the player.
|
||||
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
|
||||
*/
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
@@ -66,6 +61,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
return wrapper.duration
|
||||
}
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
return wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
@@ -84,7 +86,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { return wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
set { _wrapper.bufferDuration = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +94,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
set { _wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,22 +102,22 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
public var volume: Float {
|
||||
get { return wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
set { _wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { return wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
set { _wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
public var rate: Float {
|
||||
get { return wrapper.rate }
|
||||
set { wrapper.rate = newValue }
|
||||
set { _wrapper.rate = newValue }
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
@@ -125,14 +127,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
|
||||
*/
|
||||
public init(avPlayer: AVPlayer = AVPlayer(),
|
||||
nowPlayingInfoController: NowPlayingInfoController = NowPlayingInfoController(),
|
||||
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self.wrapper = AVPlayerWrapper(avPlayer: avPlayer)
|
||||
self._wrapper = AVPlayerWrapper()
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
self.wrapper.delegate = self
|
||||
self._wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
@@ -145,24 +146,35 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
print("Loading: \(item)")
|
||||
let url: URL
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
if let url = URL(string: item.getSourceUrl()) {
|
||||
wrapper.load(from: url, playWhenReady: playWhenReady)
|
||||
if let itemUrl = URL(string: item.getSourceUrl()) {
|
||||
url = itemUrl
|
||||
}
|
||||
else {
|
||||
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
|
||||
}
|
||||
case .file:
|
||||
wrapper.load(from: URL(fileURLWithPath: item.getSourceUrl()), playWhenReady: playWhenReady)
|
||||
url = URL(fileURLWithPath: item.getSourceUrl())
|
||||
}
|
||||
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
wrapper.load(from: url,
|
||||
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
|
||||
self.updateMetaValues(item: item)
|
||||
setArtwork(forItem: item)
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
self.loadNowPlayingMetaValues()
|
||||
}
|
||||
enableRemoteCommands(forItem: item)
|
||||
}
|
||||
|
||||
@@ -191,15 +203,18 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
Stop playback, resetting the player.
|
||||
*/
|
||||
public func stop() {
|
||||
AVWrapper(itemPlaybackDoneWithReason: .playerStopped)
|
||||
self.reset()
|
||||
self.wrapper.stop()
|
||||
self.event.playbackEnd.emit(data: .playerStopped)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek to a specific time in the item.
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
self.updateNowPlayingCurrentTime(seconds)
|
||||
}
|
||||
self.wrapper.seek(to: seconds)
|
||||
}
|
||||
|
||||
@@ -220,26 +235,54 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
/// Reload all NowPlayingInfo for the playing item.
|
||||
public func reloadNowPlayingInfo() {
|
||||
/**
|
||||
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
|
||||
|
||||
Reloads:
|
||||
- Artist
|
||||
- Title
|
||||
- Album title
|
||||
- Album artwork
|
||||
*/
|
||||
public func loadNowPlayingMetaValues() {
|
||||
guard let item = currentItem else { return }
|
||||
updateMetaValues(item: item)
|
||||
setArtwork(forItem: item)
|
||||
updatePlaybackValues()
|
||||
}
|
||||
|
||||
func updateMetaValues(item: AudioItem) {
|
||||
guard automaticallyUpdateNowPlayingInfo else { return }
|
||||
|
||||
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.artist(item.getArtist()),
|
||||
MediaItemProperty.title(item.getTitle()),
|
||||
MediaItemProperty.albumTitle(item.getAlbumTitle()),
|
||||
])
|
||||
])
|
||||
|
||||
loadArtwork(forItem: item)
|
||||
}
|
||||
|
||||
func setArtwork(forItem item: AudioItem) {
|
||||
guard automaticallyUpdateNowPlayingInfo else { return }
|
||||
/**
|
||||
Resyncs the playbackvalues of the currently playing `AudioItem`.
|
||||
|
||||
Will resync:
|
||||
- Current time
|
||||
- Duration
|
||||
- Playback rate
|
||||
*/
|
||||
public func updateNowPlayingPlaybackValues() {
|
||||
updateNowPlayingDuration(duration)
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
updateNowPlayingRate(rate)
|
||||
}
|
||||
|
||||
private func updateNowPlayingDuration(_ duration: Double) {
|
||||
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
|
||||
}
|
||||
|
||||
private func updateNowPlayingRate(_ rate: Float) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
|
||||
}
|
||||
|
||||
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
|
||||
}
|
||||
|
||||
private func loadArtwork(forItem item: AudioItem) {
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
|
||||
@@ -250,16 +293,9 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func updatePlaybackValues() {
|
||||
guard automaticallyUpdateNowPlayingInfo else { return }
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime))
|
||||
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(wrapper.duration))
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(wrapper.rate)))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func reset() {
|
||||
func reset() {
|
||||
self._currentItem = nil
|
||||
}
|
||||
|
||||
@@ -267,31 +303,45 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
switch state {
|
||||
case .playing, .paused: updatePlaybackValues()
|
||||
case .ready:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
case .playing, .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
updateNowPlayingRate(rate)
|
||||
}
|
||||
default: break
|
||||
}
|
||||
self.delegate?.audioPlayer(playerDidChangeState: state)
|
||||
}
|
||||
|
||||
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
|
||||
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: reason)
|
||||
self.event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
self.delegate?.audioPlayer(secondsElapsed: seconds)
|
||||
self.event.secondElapse.emit(data: seconds)
|
||||
}
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
self.delegate?.audioPlayer(failedWithError: error)
|
||||
self.event.fail.emit(data: error)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
self.updatePlaybackValues()
|
||||
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
|
||||
if !didFinish && automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
}
|
||||
self.event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
self.delegate?.audioPlayer(didUpdateDuration: duration)
|
||||
self.event.updateDuration.emit(data: duration)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
self.event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
self.event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,16 +13,21 @@ protocol AudioSession {
|
||||
|
||||
var isOtherAudioPlaying: Bool { get }
|
||||
|
||||
var availableCategories: [String] { get }
|
||||
var category: AVAudioSession.Category { get }
|
||||
|
||||
var mode: AVAudioSession.Mode { get }
|
||||
|
||||
func setCategory(_ category: String) throws
|
||||
var categoryOptions: AVAudioSession.CategoryOptions { get }
|
||||
|
||||
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws
|
||||
var availableCategories: [AVAudioSession.Category] { get }
|
||||
|
||||
func setActive(_ active: Bool) throws
|
||||
@available(iOS 10.0, *)
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
|
||||
|
||||
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws
|
||||
@available(iOS 11.0, *)
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
|
||||
|
||||
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// AudioSessionCategory.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 02/11/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
/**
|
||||
An enum wrapper around the AVAudioSessionCategories.
|
||||
For detailed info about the categories, see: [AudioSession Programming Guide](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10)
|
||||
*/
|
||||
public enum AudioSessionCategory {
|
||||
|
||||
case ambient
|
||||
|
||||
case soloAmbient
|
||||
|
||||
case playback
|
||||
|
||||
case record
|
||||
|
||||
case playAndRecord
|
||||
|
||||
case multiRoute
|
||||
|
||||
func getValue() -> String {
|
||||
switch self {
|
||||
|
||||
case .ambient:
|
||||
return AVAudioSessionCategoryAmbient
|
||||
|
||||
case .soloAmbient:
|
||||
return AVAudioSessionCategorySoloAmbient
|
||||
|
||||
case .playback:
|
||||
return AVAudioSessionCategoryPlayback
|
||||
|
||||
case .record:
|
||||
return AVAudioSessionCategoryRecord
|
||||
|
||||
case .playAndRecord:
|
||||
return AVAudioSessionCategoryPlayAndRecord
|
||||
|
||||
case .multiRoute:
|
||||
return AVAudioSessionCategoryMultiRoute
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user