207 lines
5.9 KiB
Swift
207 lines
5.9 KiB
Swift
import FlutterMacOS
|
|
import XCTest
|
|
|
|
@testable import Plezy
|
|
|
|
final class ControllablePropertyCore: MpvPlayerCoreBase {
|
|
var nextResult: Result<Void, Error>?
|
|
private(set) var propertyCalls: [(String, String)] = []
|
|
private var pendingCompletion: ((Result<Void, Error>) -> Void)?
|
|
|
|
override func setPropertyAsync(
|
|
_ name: String,
|
|
value: String,
|
|
completion: @escaping (Result<Void, Error>) -> Void
|
|
) {
|
|
propertyCalls.append((name, value))
|
|
if let nextResult {
|
|
self.nextResult = nil
|
|
completion(nextResult)
|
|
} else {
|
|
pendingCompletion = completion
|
|
}
|
|
}
|
|
|
|
func finish(_ result: Result<Void, Error>) {
|
|
let completion = pendingCompletion
|
|
pendingCompletion = nil
|
|
completion?(result)
|
|
}
|
|
}
|
|
|
|
final class RecordingMpvPlugin: MpvPluginShared {
|
|
var coreBase: MpvPlayerCoreBase?
|
|
var eventSink: FlutterEventSink?
|
|
var nameToId: [String: Int] = [:]
|
|
private(set) var pauseHookValues: [String] = []
|
|
|
|
init(core: MpvPlayerCoreBase?) {
|
|
coreBase = core
|
|
}
|
|
|
|
func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible: Bool) {}
|
|
func updatePlayerFrame() {}
|
|
|
|
func didSetPauseProperty(value: String) {
|
|
pauseHookValues.append(value)
|
|
}
|
|
}
|
|
|
|
final class MpvPlayerContractTests: XCTestCase {
|
|
private let failure = NSError(
|
|
domain: "MpvPlayerContractTests",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "controlled failure"]
|
|
)
|
|
|
|
func testSharedSetPropertyMapsSuccessFailureMissingCoreAndInvalidArguments() {
|
|
let core = ControllablePropertyCore()
|
|
let plugin = RecordingMpvPlugin(core: core)
|
|
|
|
core.nextResult = .success(())
|
|
let success = invokeSetProperty(plugin, name: "pause", value: "no")
|
|
XCTAssertEqual(success.count, 1)
|
|
XCTAssertNil(success[0])
|
|
XCTAssertEqual(plugin.pauseHookValues, ["no"])
|
|
|
|
core.nextResult = .failure(failure)
|
|
let rejected = invokeSetProperty(plugin, name: "pause", value: "yes")
|
|
XCTAssertEqual(rejected.count, 1)
|
|
XCTAssertEqual((rejected[0] as? FlutterError)?.code, "SET_PROPERTY_FAILED")
|
|
XCTAssertEqual(plugin.pauseHookValues, ["no"])
|
|
|
|
plugin.coreBase = nil
|
|
let missing = invokeSetProperty(plugin, name: "volume", value: "50")
|
|
XCTAssertEqual(missing.count, 1)
|
|
XCTAssertEqual((missing[0] as? FlutterError)?.code, "NOT_INITIALIZED")
|
|
|
|
var invalidResults: [Any?] = []
|
|
plugin.handleSetProperty(
|
|
call: FlutterMethodCall(methodName: "setProperty", arguments: ["name": "pause"])
|
|
) {
|
|
invalidResults.append($0)
|
|
}
|
|
XCTAssertEqual(invalidResults.count, 1)
|
|
XCTAssertEqual((invalidResults[0] as? FlutterError)?.code, "INVALID_ARGS")
|
|
}
|
|
|
|
func testRealSetPropertyValidInvalidNonexistentAndPauseCache() {
|
|
let core = MpvAudioPlayerCore()
|
|
XCTAssertTrue(core.initialize())
|
|
defer {
|
|
core.dispose()
|
|
core.queue.sync {}
|
|
}
|
|
|
|
XCTAssertSuccess(awaitProperty(core, name: "volume", value: "50"))
|
|
XCTAssertTrue(core.isPaused)
|
|
|
|
XCTAssertFailure(awaitProperty(core, name: "pause", value: "not-a-flag"))
|
|
XCTAssertTrue(core.isPaused, "A rejected raw pause write must not change the cache")
|
|
|
|
XCTAssertFailure(
|
|
awaitProperty(core, name: "plezy-property-does-not-exist", value: "ignored")
|
|
)
|
|
XCTAssertTrue(core.isPaused)
|
|
|
|
XCTAssertSuccess(awaitProperty(core, name: "pause", value: "no"))
|
|
XCTAssertFalse(core.isPaused, "The accepted pause write must commit before completion")
|
|
}
|
|
|
|
func testPendingSetPropertyIsCancelledExactlyOnceOnDispose() {
|
|
let core = MpvAudioPlayerCore()
|
|
XCTAssertTrue(core.initialize())
|
|
|
|
let queueEntered = expectation(description: "mpv queue blocked")
|
|
let releaseQueue = DispatchSemaphore(value: 0)
|
|
core.queue.async {
|
|
queueEntered.fulfill()
|
|
releaseQueue.wait()
|
|
}
|
|
wait(for: [queueEntered], timeout: 2)
|
|
|
|
let completion = expectation(description: "cancelled property completion")
|
|
completion.assertForOverFulfill = true
|
|
var completionCount = 0
|
|
core.setPropertyAsync("volume", value: "51") { result in
|
|
completionCount += 1
|
|
if case .success = result {
|
|
XCTFail("Disposal must fail an accepted-but-pending property request")
|
|
}
|
|
completion.fulfill()
|
|
}
|
|
|
|
core.dispose()
|
|
releaseQueue.signal()
|
|
wait(for: [completion], timeout: 2)
|
|
core.queue.sync {}
|
|
XCTAssertEqual(completionCount, 1)
|
|
XCTAssertFailure(awaitProperty(core, name: "volume", value: "52"))
|
|
}
|
|
|
|
func testRapidAudioCoreReplacementOwnsLifecycleOnce() {
|
|
for _ in 0..<5 {
|
|
autoreleasepool {
|
|
let core = MpvAudioPlayerCore()
|
|
XCTAssertTrue(core.initialize())
|
|
core.dispose()
|
|
core.dispose()
|
|
core.queue.sync {}
|
|
XCTAssertFalse(core.hasActiveMpv)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func invokeSetProperty(
|
|
_ plugin: RecordingMpvPlugin,
|
|
name: String,
|
|
value: String
|
|
) -> [Any?] {
|
|
var results: [Any?] = []
|
|
plugin.handleSetProperty(
|
|
call: FlutterMethodCall(
|
|
methodName: "setProperty",
|
|
arguments: ["name": name, "value": value]
|
|
)
|
|
) {
|
|
results.append($0)
|
|
}
|
|
return results
|
|
}
|
|
|
|
private func awaitProperty(
|
|
_ core: MpvPlayerCoreBase,
|
|
name: String,
|
|
value: String
|
|
) -> Result<Void, Error> {
|
|
let completion = expectation(description: "set \(name)")
|
|
var propertyResult: Result<Void, Error>?
|
|
core.setPropertyAsync(name, value: value) {
|
|
propertyResult = $0
|
|
completion.fulfill()
|
|
}
|
|
wait(for: [completion], timeout: 2)
|
|
return propertyResult ?? .failure(failure)
|
|
}
|
|
|
|
private func XCTAssertSuccess(
|
|
_ result: Result<Void, Error>,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) {
|
|
if case .failure(let error) = result {
|
|
XCTFail("Expected success, received \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
private func XCTAssertFailure(
|
|
_ result: Result<Void, Error>,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) {
|
|
if case .success = result {
|
|
XCTFail("Expected failure", file: file, line: line)
|
|
}
|
|
}
|
|
}
|