@dvai-bridge/ios 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +104 -104
- package/ios/Sources/DVAIBridge/BackendKind.swift +23 -23
- package/ios/Sources/DVAIBridge/BoundServer.swift +46 -46
- package/ios/Sources/DVAIBridge/DVAIBridge.swift +658 -658
- package/ios/Sources/DVAIBridge/DVAIBridgeConfig.swift +86 -86
- package/ios/Sources/DVAIBridge/DVAIBridgeError.swift +33 -33
- package/ios/Sources/DVAIBridge/Internal/BackendSelector.swift +59 -59
- package/ios/Sources/DVAIBridge/Internal/ProgressBroadcaster.swift +84 -84
- package/ios/Sources/DVAIBridge/License/Audience.swift +133 -133
- package/ios/Sources/DVAIBridge/License/Discovery.swift +164 -164
- package/ios/Sources/DVAIBridge/License/LicenseValidator.swift +392 -392
- package/ios/Sources/DVAIBridge/License/PublicKeys.swift +114 -114
- package/ios/Sources/DVAIBridge/License/Types.swift +195 -195
- package/ios/Sources/DVAIBridge/Offload/OffloadConfig.swift +118 -118
- package/ios/Sources/DVAIBridge/ProgressEvent.swift +34 -34
- package/ios/Sources/DVAICoreMLCore/CoreMLBackendError.swift +19 -19
- package/ios/Sources/DVAICoreMLCore/CoreMLHandlers.swift +123 -123
- package/ios/Sources/DVAICoreMLCore/CoreMLPluginState.swift +130 -130
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLEngine.swift +137 -137
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLGenerator.swift +108 -108
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLSampler.swift +96 -96
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLTokenizer.swift +69 -69
- package/ios/Tests/DVAIBridgeTests/BackendSelectorTests.swift +53 -53
- package/ios/Tests/DVAIBridgeTests/CoreMLEngineTests.swift +18 -18
- package/ios/Tests/DVAIBridgeTests/CoreMLGeneratorShapeTests.swift +11 -11
- package/ios/Tests/DVAIBridgeTests/CoreMLHandlersTests.swift +32 -32
- package/ios/Tests/DVAIBridgeTests/CoreMLPluginStateTests.swift +41 -41
- package/ios/Tests/DVAIBridgeTests/CoreMLSamplerTests.swift +40 -40
- package/ios/Tests/DVAIBridgeTests/CoreMLTokenizerTests.swift +19 -19
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeAPIShapeTests.swift +37 -37
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeConfigTests.swift +52 -52
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeErrorTests.swift +33 -33
- package/ios/Tests/DVAIBridgeTests/LicenseValidatorTests.swift +658 -658
- package/ios/Tests/DVAIBridgeTests/ProgressBroadcasterTests.swift +69 -69
- package/ios/Tests/DVAIBridgeTests/ProgressEventTests.swift +25 -25
- package/ios/Tests/DVAIBridgeTests/ReactiveStateTests.swift +45 -45
- package/ios/Tests/DVAIBridgeTests/RealModelIntegrationTest.swift +385 -359
- package/package.json +3 -4
- package/DVAIBridge.podspec +0 -120
- package/LICENSE +0 -51
- package/README.md +0 -199
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAICoreMLCore
|
|
3
|
-
|
|
4
|
-
@available(iOS 18.0, macOS 15.0, *)
|
|
5
|
-
final class CoreMLEngineTests: XCTestCase {
|
|
6
|
-
func testLoadFailsForMissingFile() {
|
|
7
|
-
let bogusURL = URL(fileURLWithPath: "/tmp/definitely-does-not-exist.mlmodelc")
|
|
8
|
-
XCTAssertThrowsError(try CoreMLEngine(modelURL: bogusURL, eosTokenId: 0)) { err in
|
|
9
|
-
guard case let CoreMLBackendError.modelLoadFailed(reason) = err else {
|
|
10
|
-
return XCTFail("wrong error: \(err)")
|
|
11
|
-
}
|
|
12
|
-
XCTAssertTrue(
|
|
13
|
-
reason.contains("error") || reason.contains("Error") || reason.contains("file"),
|
|
14
|
-
"reason should mention error details, got: \(reason)"
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAICoreMLCore
|
|
3
|
+
|
|
4
|
+
@available(iOS 18.0, macOS 15.0, *)
|
|
5
|
+
final class CoreMLEngineTests: XCTestCase {
|
|
6
|
+
func testLoadFailsForMissingFile() {
|
|
7
|
+
let bogusURL = URL(fileURLWithPath: "/tmp/definitely-does-not-exist.mlmodelc")
|
|
8
|
+
XCTAssertThrowsError(try CoreMLEngine(modelURL: bogusURL, eosTokenId: 0)) { err in
|
|
9
|
+
guard case let CoreMLBackendError.modelLoadFailed(reason) = err else {
|
|
10
|
+
return XCTFail("wrong error: \(err)")
|
|
11
|
+
}
|
|
12
|
+
XCTAssertTrue(
|
|
13
|
+
reason.contains("error") || reason.contains("Error") || reason.contains("file"),
|
|
14
|
+
"reason should mention error details, got: \(reason)"
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAICoreMLCore
|
|
3
|
-
|
|
4
|
-
@available(iOS 18.0, macOS 15.0, *)
|
|
5
|
-
final class CoreMLGeneratorShapeTests: XCTestCase {
|
|
6
|
-
func testTypesCompile() {
|
|
7
|
-
// Ensures the public-internal API compiles correctly at the type level.
|
|
8
|
-
// Real generation is tested end-to-end in RealModelIntegrationTest (Task 18).
|
|
9
|
-
let _: AsyncThrowingStream<String, Error>.Type = AsyncThrowingStream<String, Error>.self
|
|
10
|
-
}
|
|
11
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAICoreMLCore
|
|
3
|
+
|
|
4
|
+
@available(iOS 18.0, macOS 15.0, *)
|
|
5
|
+
final class CoreMLGeneratorShapeTests: XCTestCase {
|
|
6
|
+
func testTypesCompile() {
|
|
7
|
+
// Ensures the public-internal API compiles correctly at the type level.
|
|
8
|
+
// Real generation is tested end-to-end in RealModelIntegrationTest (Task 18).
|
|
9
|
+
let _: AsyncThrowingStream<String, Error>.Type = AsyncThrowingStream<String, Error>.self
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import DVAISharedCore
|
|
3
|
-
@testable import DVAICoreMLCore
|
|
4
|
-
|
|
5
|
-
@available(iOS 18.0, macOS 15.0, *)
|
|
6
|
-
final class CoreMLHandlersTests: XCTestCase {
|
|
7
|
-
func testHandleEmbeddingsReturns501() async throws {
|
|
8
|
-
// The embeddings endpoint short-circuits without invoking the generator.
|
|
9
|
-
// We verify the response shape without needing a real MLModel.
|
|
10
|
-
let response: HandlerResponse = .error(501, "embeddings not yet supported by the CoreML backend")
|
|
11
|
-
if case let .error(status, msg) = response {
|
|
12
|
-
XCTAssertEqual(status, 501)
|
|
13
|
-
XCTAssertTrue(msg.contains("embeddings"), "expected 'embeddings' in '\(msg)'")
|
|
14
|
-
} else {
|
|
15
|
-
XCTFail("expected error response")
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func testHandleModelsReturnsConfiguredModel() async throws {
|
|
20
|
-
let response: HandlerResponse = .json(200, [
|
|
21
|
-
"object": "list",
|
|
22
|
-
"data": [["id": "test-model", "object": "model", "owned_by": "dvai-bridge"]]
|
|
23
|
-
])
|
|
24
|
-
if case let .json(status, body) = response {
|
|
25
|
-
XCTAssertEqual(status, 200)
|
|
26
|
-
let dict = body as? [String: Any]
|
|
27
|
-
XCTAssertEqual(dict?["object"] as? String, "list")
|
|
28
|
-
} else {
|
|
29
|
-
XCTFail("expected json response")
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
import DVAISharedCore
|
|
3
|
+
@testable import DVAICoreMLCore
|
|
4
|
+
|
|
5
|
+
@available(iOS 18.0, macOS 15.0, *)
|
|
6
|
+
final class CoreMLHandlersTests: XCTestCase {
|
|
7
|
+
func testHandleEmbeddingsReturns501() async throws {
|
|
8
|
+
// The embeddings endpoint short-circuits without invoking the generator.
|
|
9
|
+
// We verify the response shape without needing a real MLModel.
|
|
10
|
+
let response: HandlerResponse = .error(501, "embeddings not yet supported by the CoreML backend")
|
|
11
|
+
if case let .error(status, msg) = response {
|
|
12
|
+
XCTAssertEqual(status, 501)
|
|
13
|
+
XCTAssertTrue(msg.contains("embeddings"), "expected 'embeddings' in '\(msg)'")
|
|
14
|
+
} else {
|
|
15
|
+
XCTFail("expected error response")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testHandleModelsReturnsConfiguredModel() async throws {
|
|
20
|
+
let response: HandlerResponse = .json(200, [
|
|
21
|
+
"object": "list",
|
|
22
|
+
"data": [["id": "test-model", "object": "model", "owned_by": "dvai-bridge"]]
|
|
23
|
+
])
|
|
24
|
+
if case let .json(status, body) = response {
|
|
25
|
+
XCTAssertEqual(status, 200)
|
|
26
|
+
let dict = body as? [String: Any]
|
|
27
|
+
XCTAssertEqual(dict?["object"] as? String, "list")
|
|
28
|
+
} else {
|
|
29
|
+
XCTFail("expected json response")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAICoreMLCore
|
|
3
|
-
|
|
4
|
-
@available(iOS 18.0, macOS 15.0, *)
|
|
5
|
-
final class CoreMLPluginStateTests: XCTestCase {
|
|
6
|
-
func testStartFailsWithoutModelPath() async {
|
|
7
|
-
let state = CoreMLPluginState()
|
|
8
|
-
do {
|
|
9
|
-
_ = try await state.start(opts: [:])
|
|
10
|
-
XCTFail("Expected throw")
|
|
11
|
-
} catch let err as CoreMLBackendError {
|
|
12
|
-
guard case .modelLoadFailed = err else { return XCTFail("wrong error: \(err)") }
|
|
13
|
-
// Pass — empty opts correctly throws modelLoadFailed
|
|
14
|
-
} catch {
|
|
15
|
-
XCTFail("wrong error type: \(error)")
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func testStartFailsWithoutTokenizerPath() async {
|
|
20
|
-
let state = CoreMLPluginState()
|
|
21
|
-
do {
|
|
22
|
-
_ = try await state.start(opts: ["modelPath": "/tmp/x.mlmodelc"])
|
|
23
|
-
XCTFail("Expected throw")
|
|
24
|
-
} catch let err as CoreMLBackendError {
|
|
25
|
-
guard case .tokenizerLoadFailed = err else { return XCTFail("wrong error: \(err)") }
|
|
26
|
-
// Pass — missing tokenizerPath correctly throws tokenizerLoadFailed
|
|
27
|
-
} catch {
|
|
28
|
-
XCTFail("wrong error type: \(error)")
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func testStopWhenNotStartedIsIdempotent() async throws {
|
|
33
|
-
try await CoreMLPluginState().stop()
|
|
34
|
-
// Doesn't throw — idempotent stop is required by the API contract
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
func testStatusInfoReportsNotRunning() async {
|
|
38
|
-
let info = await CoreMLPluginState().statusInfo()
|
|
39
|
-
XCTAssertEqual(info["running"] as? Bool, false)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAICoreMLCore
|
|
3
|
+
|
|
4
|
+
@available(iOS 18.0, macOS 15.0, *)
|
|
5
|
+
final class CoreMLPluginStateTests: XCTestCase {
|
|
6
|
+
func testStartFailsWithoutModelPath() async {
|
|
7
|
+
let state = CoreMLPluginState()
|
|
8
|
+
do {
|
|
9
|
+
_ = try await state.start(opts: [:])
|
|
10
|
+
XCTFail("Expected throw")
|
|
11
|
+
} catch let err as CoreMLBackendError {
|
|
12
|
+
guard case .modelLoadFailed = err else { return XCTFail("wrong error: \(err)") }
|
|
13
|
+
// Pass — empty opts correctly throws modelLoadFailed
|
|
14
|
+
} catch {
|
|
15
|
+
XCTFail("wrong error type: \(error)")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testStartFailsWithoutTokenizerPath() async {
|
|
20
|
+
let state = CoreMLPluginState()
|
|
21
|
+
do {
|
|
22
|
+
_ = try await state.start(opts: ["modelPath": "/tmp/x.mlmodelc"])
|
|
23
|
+
XCTFail("Expected throw")
|
|
24
|
+
} catch let err as CoreMLBackendError {
|
|
25
|
+
guard case .tokenizerLoadFailed = err else { return XCTFail("wrong error: \(err)") }
|
|
26
|
+
// Pass — missing tokenizerPath correctly throws tokenizerLoadFailed
|
|
27
|
+
} catch {
|
|
28
|
+
XCTFail("wrong error type: \(error)")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testStopWhenNotStartedIsIdempotent() async throws {
|
|
33
|
+
try await CoreMLPluginState().stop()
|
|
34
|
+
// Doesn't throw — idempotent stop is required by the API contract
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testStatusInfoReportsNotRunning() async {
|
|
38
|
+
let info = await CoreMLPluginState().statusInfo()
|
|
39
|
+
XCTAssertEqual(info["running"] as? Bool, false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import CoreML
|
|
3
|
-
@testable import DVAICoreMLCore
|
|
4
|
-
|
|
5
|
-
final class CoreMLSamplerTests: XCTestCase {
|
|
6
|
-
func makeLogits(_ vals: [Float]) -> MLMultiArray {
|
|
7
|
-
let arr = try! MLMultiArray(shape: [NSNumber(value: vals.count)], dataType: .float32)
|
|
8
|
-
for (i, v) in vals.enumerated() { arr[i] = NSNumber(value: v) }
|
|
9
|
-
return arr
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
func testGreedyReturnsArgmax() {
|
|
13
|
-
let s = CoreMLSampler(temperature: 0, topP: 1.0, topK: 0)
|
|
14
|
-
let logits = makeLogits([1.0, 5.0, 2.0, 4.0])
|
|
15
|
-
XCTAssertEqual(s.sample(logits: logits), 1) // argmax index
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func testTemperatureSamplingNeverThrows() {
|
|
19
|
-
let s = CoreMLSampler(temperature: 1.0, topP: 1.0, topK: 0)
|
|
20
|
-
let logits = makeLogits([1.0, 2.0, 3.0, 4.0])
|
|
21
|
-
for _ in 0 ..< 100 {
|
|
22
|
-
let token = s.sample(logits: logits)
|
|
23
|
-
XCTAssertGreaterThanOrEqual(token, 0)
|
|
24
|
-
XCTAssertLessThan(token, 4)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
func testTopPTruncationFavorsHighProb() {
|
|
29
|
-
// With top_p = 0.5 and a sharply skewed distribution, only the top
|
|
30
|
-
// few tokens should ever be selected.
|
|
31
|
-
let s = CoreMLSampler(temperature: 1.0, topP: 0.5, topK: 0)
|
|
32
|
-
// Logits chosen so that softmax(logits) ≈ [0.0, 0.0, 0.05, 0.95]
|
|
33
|
-
let logits = makeLogits([-100.0, -100.0, 1.0, 4.0])
|
|
34
|
-
var counts = [0, 0, 0, 0]
|
|
35
|
-
for _ in 0 ..< 1000 { counts[s.sample(logits: logits)] += 1 }
|
|
36
|
-
XCTAssertEqual(counts[0], 0)
|
|
37
|
-
XCTAssertEqual(counts[1], 0)
|
|
38
|
-
XCTAssertGreaterThan(counts[3], counts[2]) // 4.0 picked far more often than 1.0
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
import CoreML
|
|
3
|
+
@testable import DVAICoreMLCore
|
|
4
|
+
|
|
5
|
+
final class CoreMLSamplerTests: XCTestCase {
|
|
6
|
+
func makeLogits(_ vals: [Float]) -> MLMultiArray {
|
|
7
|
+
let arr = try! MLMultiArray(shape: [NSNumber(value: vals.count)], dataType: .float32)
|
|
8
|
+
for (i, v) in vals.enumerated() { arr[i] = NSNumber(value: v) }
|
|
9
|
+
return arr
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func testGreedyReturnsArgmax() {
|
|
13
|
+
let s = CoreMLSampler(temperature: 0, topP: 1.0, topK: 0)
|
|
14
|
+
let logits = makeLogits([1.0, 5.0, 2.0, 4.0])
|
|
15
|
+
XCTAssertEqual(s.sample(logits: logits), 1) // argmax index
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func testTemperatureSamplingNeverThrows() {
|
|
19
|
+
let s = CoreMLSampler(temperature: 1.0, topP: 1.0, topK: 0)
|
|
20
|
+
let logits = makeLogits([1.0, 2.0, 3.0, 4.0])
|
|
21
|
+
for _ in 0 ..< 100 {
|
|
22
|
+
let token = s.sample(logits: logits)
|
|
23
|
+
XCTAssertGreaterThanOrEqual(token, 0)
|
|
24
|
+
XCTAssertLessThan(token, 4)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testTopPTruncationFavorsHighProb() {
|
|
29
|
+
// With top_p = 0.5 and a sharply skewed distribution, only the top
|
|
30
|
+
// few tokens should ever be selected.
|
|
31
|
+
let s = CoreMLSampler(temperature: 1.0, topP: 0.5, topK: 0)
|
|
32
|
+
// Logits chosen so that softmax(logits) ≈ [0.0, 0.0, 0.05, 0.95]
|
|
33
|
+
let logits = makeLogits([-100.0, -100.0, 1.0, 4.0])
|
|
34
|
+
var counts = [0, 0, 0, 0]
|
|
35
|
+
for _ in 0 ..< 1000 { counts[s.sample(logits: logits)] += 1 }
|
|
36
|
+
XCTAssertEqual(counts[0], 0)
|
|
37
|
+
XCTAssertEqual(counts[1], 0)
|
|
38
|
+
XCTAssertGreaterThan(counts[3], counts[2]) // 4.0 picked far more often than 1.0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAICoreMLCore
|
|
3
|
-
|
|
4
|
-
final class CoreMLTokenizerTests: XCTestCase {
|
|
5
|
-
func testInitFailsForMissingDir() async {
|
|
6
|
-
let bogus = URL(fileURLWithPath: "/tmp/no-such-tokenizer-dir-xyz")
|
|
7
|
-
do {
|
|
8
|
-
_ = try await CoreMLTokenizer(tokenizerDir: bogus)
|
|
9
|
-
XCTFail("Expected throw for missing tokenizer dir")
|
|
10
|
-
} catch let err as CoreMLBackendError {
|
|
11
|
-
guard case .tokenizerLoadFailed = err else {
|
|
12
|
-
return XCTFail("wrong error type: \(err)")
|
|
13
|
-
}
|
|
14
|
-
// Pass — missing dir correctly converts to tokenizerLoadFailed
|
|
15
|
-
} catch {
|
|
16
|
-
XCTFail("wrong error type: \(error)")
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAICoreMLCore
|
|
3
|
+
|
|
4
|
+
final class CoreMLTokenizerTests: XCTestCase {
|
|
5
|
+
func testInitFailsForMissingDir() async {
|
|
6
|
+
let bogus = URL(fileURLWithPath: "/tmp/no-such-tokenizer-dir-xyz")
|
|
7
|
+
do {
|
|
8
|
+
_ = try await CoreMLTokenizer(tokenizerDir: bogus)
|
|
9
|
+
XCTFail("Expected throw for missing tokenizer dir")
|
|
10
|
+
} catch let err as CoreMLBackendError {
|
|
11
|
+
guard case .tokenizerLoadFailed = err else {
|
|
12
|
+
return XCTFail("wrong error type: \(err)")
|
|
13
|
+
}
|
|
14
|
+
// Pass — missing dir correctly converts to tokenizerLoadFailed
|
|
15
|
+
} catch {
|
|
16
|
+
XCTFail("wrong error type: \(error)")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAIBridge
|
|
3
|
-
|
|
4
|
-
final class DVAIBridgeAPIShapeTests: XCTestCase {
|
|
5
|
-
func testSingletonExists() {
|
|
6
|
-
let bridge: DVAIBridge = DVAIBridge.shared
|
|
7
|
-
XCTAssertNotNil(bridge)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
func testStatusBeforeStartReportsNotRunning() async {
|
|
11
|
-
let bridge = DVAIBridge() // fresh instance for test isolation
|
|
12
|
-
let info = await bridge.status()
|
|
13
|
-
XCTAssertFalse(info.running)
|
|
14
|
-
XCTAssertNil(info.backend)
|
|
15
|
-
XCTAssertNil(info.baseUrl)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func testStopWhenNotStartedIsIdempotent() async throws {
|
|
19
|
-
let bridge = DVAIBridge()
|
|
20
|
-
try await bridge.stop()
|
|
21
|
-
try await bridge.stop() // no throw
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func testStartCoreMLThrowsBackendUnavailable() async {
|
|
25
|
-
let bridge = DVAIBridge()
|
|
26
|
-
do {
|
|
27
|
-
_ = try await bridge.start(.init(backend: .coreml))
|
|
28
|
-
XCTFail("Expected throw")
|
|
29
|
-
} catch let err as DVAIBridgeError {
|
|
30
|
-
if case .backendUnavailable(.coreml, _) = err { /* expected */ } else {
|
|
31
|
-
XCTFail("wrong error: \(err)")
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
XCTFail("wrong error type: \(error)")
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAIBridge
|
|
3
|
+
|
|
4
|
+
final class DVAIBridgeAPIShapeTests: XCTestCase {
|
|
5
|
+
func testSingletonExists() {
|
|
6
|
+
let bridge: DVAIBridge = DVAIBridge.shared
|
|
7
|
+
XCTAssertNotNil(bridge)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
func testStatusBeforeStartReportsNotRunning() async {
|
|
11
|
+
let bridge = DVAIBridge() // fresh instance for test isolation
|
|
12
|
+
let info = await bridge.status()
|
|
13
|
+
XCTAssertFalse(info.running)
|
|
14
|
+
XCTAssertNil(info.backend)
|
|
15
|
+
XCTAssertNil(info.baseUrl)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func testStopWhenNotStartedIsIdempotent() async throws {
|
|
19
|
+
let bridge = DVAIBridge()
|
|
20
|
+
try await bridge.stop()
|
|
21
|
+
try await bridge.stop() // no throw
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testStartCoreMLThrowsBackendUnavailable() async {
|
|
25
|
+
let bridge = DVAIBridge()
|
|
26
|
+
do {
|
|
27
|
+
_ = try await bridge.start(.init(backend: .coreml))
|
|
28
|
+
XCTFail("Expected throw")
|
|
29
|
+
} catch let err as DVAIBridgeError {
|
|
30
|
+
if case .backendUnavailable(.coreml, _) = err { /* expected */ } else {
|
|
31
|
+
XCTFail("wrong error: \(err)")
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
XCTFail("wrong error type: \(error)")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAIBridge
|
|
3
|
-
|
|
4
|
-
final class DVAIBridgeConfigTests: XCTestCase {
|
|
5
|
-
func testDefaultsMatchSpec() {
|
|
6
|
-
let c = DVAIBridgeConfig()
|
|
7
|
-
XCTAssertEqual(c.backend, .auto)
|
|
8
|
-
XCTAssertNil(c.modelPath)
|
|
9
|
-
XCTAssertEqual(c.gpuLayers, 99)
|
|
10
|
-
XCTAssertEqual(c.contextSize, 2048)
|
|
11
|
-
XCTAssertEqual(c.threads, 4)
|
|
12
|
-
XCTAssertFalse(c.embeddingMode)
|
|
13
|
-
XCTAssertEqual(c.httpBasePort, 38883)
|
|
14
|
-
XCTAssertEqual(c.httpMaxPortAttempts, 16)
|
|
15
|
-
XCTAssertFalse(c.autoUnloadOnLowMemory)
|
|
16
|
-
XCTAssertEqual(c.logLevel, "info")
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func testToCoreOptsWildcardCors() {
|
|
20
|
-
let c = DVAIBridgeConfig(modelPath: "/x.gguf")
|
|
21
|
-
let opts = c.toCoreOpts()
|
|
22
|
-
XCTAssertEqual(opts["modelPath"] as? String, "/x.gguf")
|
|
23
|
-
XCTAssertEqual(opts["corsOrigin"] as? String, "*")
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
func testToCoreOptsExactCors() {
|
|
27
|
-
let c = DVAIBridgeConfig(corsOrigin: .exact("https://example.com"))
|
|
28
|
-
XCTAssertEqual(c.toCoreOpts()["corsOrigin"] as? String, "https://example.com")
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func testToCoreOptsAllowlistCors() {
|
|
32
|
-
let c = DVAIBridgeConfig(corsOrigin: .allowlist(["https://a.com", "https://b.com"]))
|
|
33
|
-
XCTAssertEqual(c.toCoreOpts()["corsOrigin"] as? [String], ["https://a.com", "https://b.com"])
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func testBoundServerInitFromCoreResult() throws {
|
|
37
|
-
let result: [String: Any] = [
|
|
38
|
-
"baseUrl": "http://127.0.0.1:38883/v1",
|
|
39
|
-
"port": 38883,
|
|
40
|
-
"modelId": "test-model"
|
|
41
|
-
]
|
|
42
|
-
let server = try BoundServer(coreResult: result, backend: .llama)
|
|
43
|
-
XCTAssertEqual(server.baseUrl, "http://127.0.0.1:38883/v1")
|
|
44
|
-
XCTAssertEqual(server.port, 38883)
|
|
45
|
-
XCTAssertEqual(server.backend, .llama)
|
|
46
|
-
XCTAssertEqual(server.modelId, "test-model")
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
func testBoundServerInitMalformedResult() {
|
|
50
|
-
XCTAssertThrowsError(try BoundServer(coreResult: ["baseUrl": "x"], backend: .llama))
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAIBridge
|
|
3
|
+
|
|
4
|
+
final class DVAIBridgeConfigTests: XCTestCase {
|
|
5
|
+
func testDefaultsMatchSpec() {
|
|
6
|
+
let c = DVAIBridgeConfig()
|
|
7
|
+
XCTAssertEqual(c.backend, .auto)
|
|
8
|
+
XCTAssertNil(c.modelPath)
|
|
9
|
+
XCTAssertEqual(c.gpuLayers, 99)
|
|
10
|
+
XCTAssertEqual(c.contextSize, 2048)
|
|
11
|
+
XCTAssertEqual(c.threads, 4)
|
|
12
|
+
XCTAssertFalse(c.embeddingMode)
|
|
13
|
+
XCTAssertEqual(c.httpBasePort, 38883)
|
|
14
|
+
XCTAssertEqual(c.httpMaxPortAttempts, 16)
|
|
15
|
+
XCTAssertFalse(c.autoUnloadOnLowMemory)
|
|
16
|
+
XCTAssertEqual(c.logLevel, "info")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testToCoreOptsWildcardCors() {
|
|
20
|
+
let c = DVAIBridgeConfig(modelPath: "/x.gguf")
|
|
21
|
+
let opts = c.toCoreOpts()
|
|
22
|
+
XCTAssertEqual(opts["modelPath"] as? String, "/x.gguf")
|
|
23
|
+
XCTAssertEqual(opts["corsOrigin"] as? String, "*")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func testToCoreOptsExactCors() {
|
|
27
|
+
let c = DVAIBridgeConfig(corsOrigin: .exact("https://example.com"))
|
|
28
|
+
XCTAssertEqual(c.toCoreOpts()["corsOrigin"] as? String, "https://example.com")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func testToCoreOptsAllowlistCors() {
|
|
32
|
+
let c = DVAIBridgeConfig(corsOrigin: .allowlist(["https://a.com", "https://b.com"]))
|
|
33
|
+
XCTAssertEqual(c.toCoreOpts()["corsOrigin"] as? [String], ["https://a.com", "https://b.com"])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testBoundServerInitFromCoreResult() throws {
|
|
37
|
+
let result: [String: Any] = [
|
|
38
|
+
"baseUrl": "http://127.0.0.1:38883/v1",
|
|
39
|
+
"port": 38883,
|
|
40
|
+
"modelId": "test-model"
|
|
41
|
+
]
|
|
42
|
+
let server = try BoundServer(coreResult: result, backend: .llama)
|
|
43
|
+
XCTAssertEqual(server.baseUrl, "http://127.0.0.1:38883/v1")
|
|
44
|
+
XCTAssertEqual(server.port, 38883)
|
|
45
|
+
XCTAssertEqual(server.backend, .llama)
|
|
46
|
+
XCTAssertEqual(server.modelId, "test-model")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func testBoundServerInitMalformedResult() {
|
|
50
|
+
XCTAssertThrowsError(try BoundServer(coreResult: ["baseUrl": "x"], backend: .llama))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAIBridge
|
|
3
|
-
|
|
4
|
-
final class DVAIBridgeErrorTests: XCTestCase {
|
|
5
|
-
func testErrorDescriptionsAreUserFacing() {
|
|
6
|
-
let cases: [(DVAIBridgeError, String)] = [
|
|
7
|
-
(.notStarted, "has not been started"),
|
|
8
|
-
(.alreadyStarted(currentBackend: .llama, baseUrl: "http://127.0.0.1:38883/v1"), "already running"),
|
|
9
|
-
(.configurationInvalid(reason: "x"), "invalid"),
|
|
10
|
-
(.backendUnavailable(.foundation, reason: "iOS 26+ required"), "unavailable"),
|
|
11
|
-
(.modelLoadFailed(reason: "x"), "Model load failed"),
|
|
12
|
-
(.downloadFailed(reason: "x"), "Download failed"),
|
|
13
|
-
(.checksumMismatch, "SHA-256"),
|
|
14
|
-
(.backendError(underlying: "x"), "Backend error"),
|
|
15
|
-
]
|
|
16
|
-
for (err, expectedFragment) in cases {
|
|
17
|
-
XCTAssertNotNil(err.errorDescription, "error has no description: \(err)")
|
|
18
|
-
XCTAssertTrue(
|
|
19
|
-
err.errorDescription!.contains(expectedFragment),
|
|
20
|
-
"expected '\(expectedFragment)' in '\(err.errorDescription!)'"
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func testBackendKindAllCases() {
|
|
26
|
-
XCTAssertEqual(BackendKind.allCases.count, 5)
|
|
27
|
-
XCTAssertTrue(BackendKind.allCases.contains(.auto))
|
|
28
|
-
XCTAssertTrue(BackendKind.allCases.contains(.llama))
|
|
29
|
-
XCTAssertTrue(BackendKind.allCases.contains(.foundation))
|
|
30
|
-
XCTAssertTrue(BackendKind.allCases.contains(.coreml))
|
|
31
|
-
XCTAssertTrue(BackendKind.allCases.contains(.mlx))
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAIBridge
|
|
3
|
+
|
|
4
|
+
final class DVAIBridgeErrorTests: XCTestCase {
|
|
5
|
+
func testErrorDescriptionsAreUserFacing() {
|
|
6
|
+
let cases: [(DVAIBridgeError, String)] = [
|
|
7
|
+
(.notStarted, "has not been started"),
|
|
8
|
+
(.alreadyStarted(currentBackend: .llama, baseUrl: "http://127.0.0.1:38883/v1"), "already running"),
|
|
9
|
+
(.configurationInvalid(reason: "x"), "invalid"),
|
|
10
|
+
(.backendUnavailable(.foundation, reason: "iOS 26+ required"), "unavailable"),
|
|
11
|
+
(.modelLoadFailed(reason: "x"), "Model load failed"),
|
|
12
|
+
(.downloadFailed(reason: "x"), "Download failed"),
|
|
13
|
+
(.checksumMismatch, "SHA-256"),
|
|
14
|
+
(.backendError(underlying: "x"), "Backend error"),
|
|
15
|
+
]
|
|
16
|
+
for (err, expectedFragment) in cases {
|
|
17
|
+
XCTAssertNotNil(err.errorDescription, "error has no description: \(err)")
|
|
18
|
+
XCTAssertTrue(
|
|
19
|
+
err.errorDescription!.contains(expectedFragment),
|
|
20
|
+
"expected '\(expectedFragment)' in '\(err.errorDescription!)'"
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testBackendKindAllCases() {
|
|
26
|
+
XCTAssertEqual(BackendKind.allCases.count, 5)
|
|
27
|
+
XCTAssertTrue(BackendKind.allCases.contains(.auto))
|
|
28
|
+
XCTAssertTrue(BackendKind.allCases.contains(.llama))
|
|
29
|
+
XCTAssertTrue(BackendKind.allCases.contains(.foundation))
|
|
30
|
+
XCTAssertTrue(BackendKind.allCases.contains(.coreml))
|
|
31
|
+
XCTAssertTrue(BackendKind.allCases.contains(.mlx))
|
|
32
|
+
}
|
|
33
|
+
}
|