mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
Replace the Go backend with the Rust workspace and route Android/iOS through UniFFI bindings. Include the migrated extension runtime, network, providers, media metadata, downloads and library operations, with version 5.0.0+147. Remove Go sources, adapters, native build selection and CI dependencies. Build Rust unconditionally and lock the iOS Rust pod using a relative path. Retain legacy source and migration evidence in a local ignored archive. Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI build checkpoint; no new APK or iOS application build for this cleanup. Known follow-up: URL/URLSearchParams globals are missing from the Rust JS runtime. A controlled extension replay confirms a URL-resolution regression; this commit does not fix that runtime gap.
403 lines
18 KiB
Swift
403 lines
18 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import Runner
|
|
|
|
class RunnerTests: XCTestCase {
|
|
private func isSymbolicLink(_ url: URL) -> Bool {
|
|
guard let values = try? url.resourceValues(forKeys: [.isSymbolicLinkKey]) else { return false }
|
|
return values.isSymbolicLink == true
|
|
}
|
|
|
|
func testFFmpegPublishesOnlySuccessfulUncancelledOutput() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let target = root.appendingPathComponent("output.flac")
|
|
for outcome in ["cancel", "failure", "success"] {
|
|
try Data("original".utf8).write(to: target)
|
|
var cancelled = false
|
|
var staged: URL?
|
|
let command = CoreFFmpegCommand(command_id: outcome, arguments: ["-i", "input.wav", "-y", target.path], output_path: target.path)
|
|
let result = executeCoreFFmpegCommand(command: command, cancelled: { cancelled }, execute: { arguments, _ in
|
|
XCTAssertEqual(arguments[1], "input.wav")
|
|
staged = URL(fileURLWithPath: arguments.last!)
|
|
XCTAssertNotEqual(staged, target)
|
|
XCTAssertEqual(staged?.pathExtension, "flac")
|
|
do { try Data("converted".utf8).write(to: staged!) }
|
|
catch { XCTFail("staging write failed: \(error)"); return (false, "write failed") }
|
|
cancelled = outcome == "cancel"
|
|
return (outcome != "failure", "result")
|
|
})
|
|
XCTAssertEqual(result.0, outcome == "success")
|
|
XCTAssertEqual(try String(contentsOf: target, encoding: .utf8), outcome == "success" ? "converted" : "original")
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: staged!.path))
|
|
}
|
|
}
|
|
|
|
func testFFmpegCallerReturnKeepsOtherClaimedCommandAlive() {
|
|
let started = DispatchSemaphore(value: 0)
|
|
let release = DispatchSemaphore(value: 0)
|
|
let closed = expectation(description: "command handle released")
|
|
let lock = NSLock()
|
|
var isClosed = false
|
|
let execution = CoreFFmpegExecution(wait: { _ in
|
|
[CoreFFmpegCommand(command_id: "other", arguments: ["convert"])]
|
|
}, active: { _ in true }, complete: { id, success, _, _ in
|
|
XCTAssertEqual(id, "other")
|
|
XCTAssertTrue(success)
|
|
}, close: {
|
|
lock.lock()
|
|
isClosed = true
|
|
lock.unlock()
|
|
closed.fulfill()
|
|
})
|
|
let result = execution.run(execute: { _, cancelled in
|
|
started.signal()
|
|
XCTAssertEqual(release.wait(timeout: .now() + 2), .success)
|
|
XCTAssertFalse(cancelled())
|
|
return (true, "finished")
|
|
}) {
|
|
XCTAssertEqual(started.wait(timeout: .now() + 2), .success)
|
|
return "caller finished"
|
|
}
|
|
XCTAssertEqual(result, "caller finished")
|
|
lock.lock()
|
|
XCTAssertFalse(isClosed)
|
|
lock.unlock()
|
|
release.signal()
|
|
wait(for: [closed], timeout: 2)
|
|
}
|
|
|
|
func testFFmpegFirstExecutionSweepsOnlyOwnOrphanFilesAndPreservesOtherEntries() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let prefix = ".spotiflac-ffmpeg-\(Bundle.main.bundleIdentifier ?? "spotiflac")-"
|
|
let ownOrphan = root.appendingPathComponent("\(prefix)\(UUID().uuidString).flac")
|
|
try Data("orphan".utf8).write(to: ownOrphan)
|
|
let foreignFile = root.appendingPathComponent(".spotiflac-ffmpeg-foreign-\(UUID().uuidString).flac")
|
|
try Data("foreign".utf8).write(to: foreignFile)
|
|
let malformedFile = root.appendingPathComponent("\(prefix)not-a-uuid.flac")
|
|
try Data("malformed".utf8).write(to: malformedFile)
|
|
let preservedDirectory = root.appendingPathComponent("\(prefix)\(UUID().uuidString).flac")
|
|
try FileManager.default.createDirectory(at: preservedDirectory, withIntermediateDirectories: false)
|
|
let directoryContent = preservedDirectory.appendingPathComponent("content.txt")
|
|
try Data("directory content".utf8).write(to: directoryContent)
|
|
let symlinkTarget = root.appendingPathComponent("symlink-target.txt")
|
|
try Data("symlink content".utf8).write(to: symlinkTarget)
|
|
let symlink = root.appendingPathComponent("\(prefix)\(UUID().uuidString).flac")
|
|
try FileManager.default.createSymbolicLink(at: symlink, withDestinationURL: symlinkTarget)
|
|
|
|
let target = root.appendingPathComponent("output.flac")
|
|
let command = CoreFFmpegCommand(
|
|
command_id: "sweep",
|
|
arguments: ["convert", target.path],
|
|
output_path: target.path
|
|
)
|
|
var staged: URL?
|
|
let result = executeCoreFFmpegCommand(command: command, cancelled: { false }) { arguments, _ in
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: ownOrphan.path))
|
|
XCTAssertEqual(try? String(contentsOf: foreignFile, encoding: .utf8), "foreign")
|
|
XCTAssertEqual(try? String(contentsOf: malformedFile, encoding: .utf8), "malformed")
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: preservedDirectory.path))
|
|
XCTAssertEqual(try? String(contentsOf: directoryContent, encoding: .utf8), "directory content")
|
|
XCTAssertTrue(isSymbolicLink(symlink))
|
|
XCTAssertEqual(try? String(contentsOf: symlink, encoding: .utf8), "symlink content")
|
|
|
|
staged = URL(fileURLWithPath: arguments.last!)
|
|
do {
|
|
try Data("published".utf8).write(to: staged!)
|
|
} catch {
|
|
XCTFail("staging write failed: \(error)")
|
|
return (false, "write failed")
|
|
}
|
|
return (true, "converted")
|
|
}
|
|
|
|
XCTAssertTrue(result.0)
|
|
XCTAssertEqual(try String(contentsOf: target, encoding: .utf8), "published")
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: staged!.path))
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: ownOrphan.path))
|
|
XCTAssertEqual(try String(contentsOf: foreignFile, encoding: .utf8), "foreign")
|
|
XCTAssertEqual(try String(contentsOf: malformedFile, encoding: .utf8), "malformed")
|
|
XCTAssertEqual(try String(contentsOf: directoryContent, encoding: .utf8), "directory content")
|
|
XCTAssertTrue(isSymbolicLink(symlink))
|
|
XCTAssertEqual(try String(contentsOf: symlink, encoding: .utf8), "symlink content")
|
|
}
|
|
|
|
func testFFmpegOverlappingExecutionsPreserveLiveStagingAndPublishBothOutputs() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let firstTarget = root.appendingPathComponent("first.flac")
|
|
let secondTarget = root.appendingPathComponent("second.flac")
|
|
let firstCommand = CoreFFmpegCommand(
|
|
command_id: "first",
|
|
arguments: ["convert-first", firstTarget.path],
|
|
output_path: firstTarget.path
|
|
)
|
|
let secondCommand = CoreFFmpegCommand(
|
|
command_id: "second",
|
|
arguments: ["convert-second", secondTarget.path],
|
|
output_path: secondTarget.path
|
|
)
|
|
var firstStage: URL?
|
|
var secondStage: URL?
|
|
|
|
let result = executeCoreFFmpegCommand(command: firstCommand, cancelled: { false }) { arguments, _ in
|
|
firstStage = URL(fileURLWithPath: arguments.last!)
|
|
do {
|
|
try Data("first staged".utf8).write(to: firstStage!)
|
|
} catch {
|
|
XCTFail("first staging write failed: \(error)")
|
|
return (false, "write failed")
|
|
}
|
|
|
|
let nestedResult = executeCoreFFmpegCommand(command: secondCommand, cancelled: { false }) { nestedArguments, _ in
|
|
secondStage = URL(fileURLWithPath: nestedArguments.last!)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstStage!.path))
|
|
XCTAssertEqual(try? String(contentsOf: firstStage!, encoding: .utf8), "first staged")
|
|
do {
|
|
try Data("second published".utf8).write(to: secondStage!)
|
|
} catch {
|
|
XCTFail("second staging write failed: \(error)")
|
|
return (false, "write failed")
|
|
}
|
|
return (true, "second complete")
|
|
}
|
|
|
|
XCTAssertTrue(nestedResult.0)
|
|
XCTAssertEqual(try? String(contentsOf: secondTarget, encoding: .utf8), "second published")
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstStage!.path))
|
|
XCTAssertEqual(try? String(contentsOf: firstStage!, encoding: .utf8), "first staged")
|
|
return (true, "first complete")
|
|
}
|
|
|
|
XCTAssertTrue(result.0)
|
|
XCTAssertEqual(try String(contentsOf: firstTarget, encoding: .utf8), "first staged")
|
|
XCTAssertEqual(try String(contentsOf: secondTarget, encoding: .utf8), "second published")
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: firstStage!.path))
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: secondStage!.path))
|
|
}
|
|
|
|
func testFFmpegCancellationAndInvalidArgumentsDoNotStopOtherCommands() {
|
|
let started = DispatchSemaphore(value: 0)
|
|
let release = DispatchSemaphore(value: 0)
|
|
let completed = DispatchSemaphore(value: 0)
|
|
let closed = expectation(description: "pump closed")
|
|
let lock = NSLock()
|
|
var active = Set(["empty", "a", "b"])
|
|
var claimed = false
|
|
let execution = CoreFFmpegExecution(wait: { _ in
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
if claimed { return [] }
|
|
claimed = true
|
|
return [CoreFFmpegCommand(command_id: "empty", arguments: []),
|
|
CoreFFmpegCommand(command_id: "a", arguments: ["a"]),
|
|
CoreFFmpegCommand(command_id: "b", arguments: ["b"])]
|
|
}, active: { id in
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return active.contains(id)
|
|
}, complete: { id, success, _, error in
|
|
if id == "empty" { XCTAssertEqual(error, "FFmpeg arguments are empty") }
|
|
if id == "a" { XCTAssertFalse(success) }
|
|
if id == "b" {
|
|
XCTAssertTrue(success)
|
|
completed.signal()
|
|
}
|
|
}, close: { closed.fulfill() })
|
|
_ = execution.run(execute: { arguments, cancelled in
|
|
if arguments == ["a"] {
|
|
started.signal()
|
|
XCTAssertEqual(release.wait(timeout: .now() + 2), .success)
|
|
XCTAssertTrue(cancelled())
|
|
return (false, "cancelled")
|
|
}
|
|
XCTAssertEqual(arguments, ["b"])
|
|
XCTAssertFalse(cancelled())
|
|
return (true, "finished")
|
|
}) {
|
|
XCTAssertEqual(started.wait(timeout: .now() + 2), .success)
|
|
lock.lock()
|
|
active.remove("a")
|
|
lock.unlock()
|
|
release.signal()
|
|
XCTAssertEqual(completed.wait(timeout: .now() + 2), .success)
|
|
return "finished"
|
|
}
|
|
wait(for: [closed], timeout: 2)
|
|
}
|
|
|
|
func testFFmpegClosedRegistryReleasesHandle() {
|
|
let closed = DispatchSemaphore(value: 0)
|
|
let execution = CoreFFmpegExecution(wait: { _ in
|
|
throw NSError(domain: "closed owner", code: 1)
|
|
}, active: { _ in false }, complete: { _, _, _, _ in
|
|
XCTFail("Closed registry produced a command")
|
|
}, close: { closed.signal() })
|
|
_ = execution.run {
|
|
XCTAssertEqual(closed.wait(timeout: .now() + 2), .success)
|
|
return "finished"
|
|
}
|
|
}
|
|
|
|
func testProgressReconnectResetsCursorAfterOwnerShutdown() {
|
|
let first = expectation(description: "first owner delivered")
|
|
let replacement = expectation(description: "replacement owner delivered")
|
|
let lock = NSLock()
|
|
var connections = 0
|
|
var closes = 0
|
|
var cursors = [Int64]()
|
|
let stream = DownloadProgressSubscription(interval: 0.01, connect: {
|
|
lock.lock()
|
|
connections += 1
|
|
let owner = connections
|
|
lock.unlock()
|
|
return CoreDownloadProgress(wait: { sequence, _ in
|
|
lock.lock()
|
|
cursors.append(sequence)
|
|
lock.unlock()
|
|
if owner == 1 && sequence == 0 { return "{\"seq\":99,\"reset\":true,\"items\":{}}" }
|
|
if owner == 1 { throw NSError(domain: "closed owner", code: 1) }
|
|
return "{\"seq\":1,\"reset\":true,\"items\":{}}"
|
|
}, close: {
|
|
lock.lock()
|
|
closes += 1
|
|
lock.unlock()
|
|
})
|
|
})
|
|
stream.start { event in
|
|
let sequence = (event as? [String: Any])?["seq"] as? Int
|
|
if sequence == 99 { first.fulfill() }
|
|
if sequence == 1 { replacement.fulfill() }
|
|
}
|
|
wait(for: [first, replacement], timeout: 2)
|
|
stream.stop()
|
|
lock.lock()
|
|
XCTAssertEqual(connections, 2)
|
|
XCTAssertEqual(closes, 2)
|
|
XCTAssertEqual(Array(cursors.prefix(3)), [0, 99, 0])
|
|
lock.unlock()
|
|
}
|
|
|
|
func testProgressStopClosesBlockedOwnerSubscription() {
|
|
let started = expectation(description: "wait started")
|
|
let stopped = expectation(description: "wait stopped")
|
|
let released = DispatchSemaphore(value: 0)
|
|
let stream = DownloadProgressSubscription(connect: {
|
|
CoreDownloadProgress(wait: { _, _ in
|
|
started.fulfill()
|
|
XCTAssertEqual(released.wait(timeout: .now() + 2), .success)
|
|
stopped.fulfill()
|
|
return "{\"seq\":1,\"items\":{}}"
|
|
}, close: { released.signal() })
|
|
})
|
|
stream.start { _ in XCTFail("Stopped listener received an event") }
|
|
wait(for: [started], timeout: 2)
|
|
stream.stop()
|
|
wait(for: [stopped], timeout: 2)
|
|
}
|
|
|
|
func testProgressRestartDoesNotAcceptCancelledWaiterState() {
|
|
let oldStarted = expectation(description: "old waiter started")
|
|
let replacementDelivered = expectation(description: "replacement snapshot")
|
|
let replacementAdvanced = expectation(description: "replacement owns cursor")
|
|
let oldReleased = expectation(description: "old waiter returned")
|
|
let releaseOld = DispatchSemaphore(value: 0)
|
|
let lock = NSLock()
|
|
var calls = 0
|
|
var invalidCursors = [Int64]()
|
|
let stream = DownloadProgressSubscription(interval: 0.01) { sequence, _ in
|
|
lock.lock()
|
|
calls += 1
|
|
let call = calls
|
|
if (call <= 2 && sequence != 0) || (call > 2 && sequence != 2) {
|
|
invalidCursors.append(sequence)
|
|
}
|
|
lock.unlock()
|
|
if call == 1 {
|
|
oldStarted.fulfill()
|
|
_ = releaseOld.wait(timeout: .now() + 3)
|
|
oldReleased.fulfill()
|
|
return "{\"seq\":99,\"items\":{}}"
|
|
}
|
|
if call == 2 { return "{\"seq\":2,\"reset\":true,\"items\":{}}" }
|
|
if call == 3 { replacementAdvanced.fulfill() }
|
|
return ""
|
|
}
|
|
stream.start { _ in XCTFail("Cancelled listener received an event") }
|
|
wait(for: [oldStarted], timeout: 2)
|
|
stream.stop()
|
|
stream.start { event in
|
|
XCTAssertEqual((event as? [String: Any])?["seq"] as? Int, 2)
|
|
replacementDelivered.fulfill()
|
|
releaseOld.signal()
|
|
}
|
|
wait(for: [replacementDelivered, oldReleased, replacementAdvanced], timeout: 2)
|
|
stream.stop()
|
|
lock.lock()
|
|
XCTAssertTrue(invalidCursors.isEmpty, "Unexpected cursors: \(invalidCursors)")
|
|
lock.unlock()
|
|
}
|
|
|
|
func testParsesOAuthCallback() {
|
|
let route = ExtensionCallbackParser.parse(
|
|
URL(string: "spotiflac://callback?code=auth-code&state=metadata-provider")!
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
route,
|
|
ExtensionCallbackRoute(
|
|
code: "auth-code",
|
|
state: "metadata-provider",
|
|
isSessionGrant: false
|
|
)
|
|
)
|
|
}
|
|
|
|
func testParsesSignedSessionGrant() {
|
|
let route = ExtensionCallbackParser.parse(
|
|
URL(
|
|
string:
|
|
"spotiflac://session-grant?grant=session-token&state=provider"
|
|
)!
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
route,
|
|
ExtensionCallbackRoute(
|
|
code: "session-token",
|
|
state: "provider",
|
|
isSessionGrant: true
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRejectsUntrustedOrIncompleteCallbacks() {
|
|
XCTAssertNil(
|
|
ExtensionCallbackParser.parse(
|
|
URL(string: "https://callback?code=auth&state=provider")!
|
|
)
|
|
)
|
|
XCTAssertNil(
|
|
ExtensionCallbackParser.parse(
|
|
URL(string: "spotiflac://unknown?code=auth&state=provider")!
|
|
)
|
|
)
|
|
XCTAssertNil(
|
|
ExtensionCallbackParser.parse(
|
|
URL(string: "spotiflac://callback?code=auth")!
|
|
)
|
|
)
|
|
XCTAssertNil(
|
|
ExtensionCallbackParser.parse(
|
|
URL(string: "spotiflac://callback?state=provider")!
|
|
)
|
|
)
|
|
}
|
|
}
|