From d4563590c98f663f5d3789bae839e9549014e623 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 28 Feb 2024 04:15:29 +0900 Subject: [PATCH 01/18] resolve sid --- LiveKit.xctestplan | 45 ++++++++++++++++++++ Tests/LiveKitTests/Basic.swift | 6 ++- Tests/LiveKitTests/PublishTests.swift | 61 +++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 LiveKit.xctestplan create mode 100644 Tests/LiveKitTests/PublishTests.swift diff --git a/LiveKit.xctestplan b/LiveKit.xctestplan new file mode 100644 index 000000000..9f2001a31 --- /dev/null +++ b/LiveKit.xctestplan @@ -0,0 +1,45 @@ +{ + "configurations" : [ + { + "id" : "C13DBD7E-A26D-4166-987B-8BB0E3A8A56F", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "environmentVariableEntries" : [ + { + "key" : "LIVEKIT_URL", + "value" : "$(LIVEKIT_URL)" + }, + { + "key" : "LIVEKIT_TOKEN", + "value" : "$(LIVEKIT_TOKEN)" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:", + "identifier" : "LiveKit", + "name" : "LiveKit" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "LiveKitTests", + "name" : "LiveKitTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "LiveKitTestsObjC", + "name" : "LiveKitTestsObjC" + } + } + ], + "version" : 1 +} diff --git a/Tests/LiveKitTests/Basic.swift b/Tests/LiveKitTests/Basic.swift index d34e38c5a..61eb09007 100644 --- a/Tests/LiveKitTests/Basic.swift +++ b/Tests/LiveKitTests/Basic.swift @@ -17,4 +17,8 @@ @testable import LiveKit import XCTest -class Basic: XCTestCase {} +class Basic: XCTestCase { + func testReadVersion() { + print("LiveKitSDK.version: \(LiveKitSDK.version)") + } +} diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift new file mode 100644 index 000000000..11ed2e3f8 --- /dev/null +++ b/Tests/LiveKitTests/PublishTests.swift @@ -0,0 +1,61 @@ +/* + * Copyright 2024 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@testable import LiveKit +import XCTest + +class PublishTests: XCTestCase { + let room = Room() + + override func setUp() async throws { + let url = ProcessInfo.processInfo.environment["LIVEKIT_URL"] + let token = ProcessInfo.processInfo.environment["LIVEKIT_TOKEN"] + + guard let url else { + XCTFail("LIVEKIT_URL is nil") + return + } + + guard let token else { + XCTFail("LIVEKIT_TOKEN is nil") + return + } + + try await room.connect(url: url, token: token) + } + + override func tearDown() async throws { + await room.disconnect() + } + + func testResolveSid() async throws { + XCTAssert(room.connectionState == .connected) + + let sid = try await room.sid() + print("Room.sid(): \(String(describing: sid))") + XCTAssert(sid.stringValue.starts(with: "RM_")) + } + + func testPublishMic() async throws { + XCTAssert(room.connectionState == .connected) + + try await room.localParticipant.setMicrophone(enabled: true) + sleep(5) + + let stats = room.localParticipant.localAudioTracks.first?.track?.statistics + print("Stats: \(String(describing: stats))") + } +} From 28926d4f2f77a5e4c1572ab1375c13351e207a27 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 28 Feb 2024 04:23:48 +0900 Subject: [PATCH 02/18] pass secrets --- .github/workflows/testing-matrix.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing-matrix.yaml b/.github/workflows/testing-matrix.yaml index 671f4d369..78735054d 100644 --- a/.github/workflows/testing-matrix.yaml +++ b/.github/workflows/testing-matrix.yaml @@ -48,4 +48,7 @@ jobs: run: xcodebuild -scheme LiveKit -showdestinations - name: Run All Tests - run: xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' + env: + LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }} + LIVEKIT_TOKEN: ${{ secrets.LIVEKIT_TOKEN }} + run: xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' LIVEKIT_URL='$LIVEKIT_URL' LIVEKIT_TOKEN='$LIVEKIT_TOKEN' From 2022f05d6afc3a7da9e64c4a4e38ee9c548d8b85 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:35:42 +0900 Subject: [PATCH 03/18] generate token --- .github/workflows/testing-matrix.yaml | 22 ++- LiveKit.xctestplan | 12 +- Package.swift | 17 ++- Sources/LiveKit/Core/Transport.swift | 1 - Tests/LiveKitTests/PublishTests.swift | 20 ++- .../LiveKitTests/Support/TokenGenerator.swift | 136 ++++++++++++++++++ Tests/LiveKitTestsObjC/Basic.m | 2 +- 7 files changed, 188 insertions(+), 22 deletions(-) create mode 100644 Tests/LiveKitTests/Support/TokenGenerator.swift diff --git a/.github/workflows/testing-matrix.yaml b/.github/workflows/testing-matrix.yaml index 78735054d..3c12e1630 100644 --- a/.github/workflows/testing-matrix.yaml +++ b/.github/workflows/testing-matrix.yaml @@ -3,9 +3,9 @@ name: Testing Matrix on: workflow_dispatch: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -17,7 +17,12 @@ jobs: fail-fast: false matrix: xcode-version: [14.2, 15.2] - destination: ['platform=iOS Simulator,OS=17.2,name=iPhone 14 Pro', 'platform=macOS', 'platform=macOS,variant=Mac Catalyst'] + destination: + [ + "platform=iOS Simulator,OS=17.2,name=iPhone 14 Pro", + "platform=macOS", + "platform=macOS,variant=Mac Catalyst", + ] runs-on: macos-13 @@ -49,6 +54,11 @@ jobs: - name: Run All Tests env: - LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }} - LIVEKIT_TOKEN: ${{ secrets.LIVEKIT_TOKEN }} - run: xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' LIVEKIT_URL='$LIVEKIT_URL' LIVEKIT_TOKEN='$LIVEKIT_TOKEN' + LIVEKIT_TESTING_URL: ${{ secrets.LIVEKIT_TESTING_URL }} + LIVEKIT_TESTING_API_KEY: ${{ secrets.LIVEKIT_TESTING_API_KEY }} + LIVEKIT_TESTING_API_SECRET: ${{ secrets.LIVEKIT_TESTING_API_SECRET }} + run: | + xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' \ + LIVEKIT_TESTING_URL='$LIVEKIT_TESTING_URL' \ + LIVEKIT_TESTING_API_KEY='$LIVEKIT_TESTING_API_KEY' \ + LIVEKIT_TESTING_API_SECRET='$LIVEKIT_TESTING_API_SECRET' diff --git a/LiveKit.xctestplan b/LiveKit.xctestplan index 9f2001a31..c79a61dbf 100644 --- a/LiveKit.xctestplan +++ b/LiveKit.xctestplan @@ -11,12 +11,16 @@ "defaultOptions" : { "environmentVariableEntries" : [ { - "key" : "LIVEKIT_URL", - "value" : "$(LIVEKIT_URL)" + "key" : "LIVEKIT_TESTING_URL", + "value" : "$(LIVEKIT_TESTING_URL)" }, { - "key" : "LIVEKIT_TOKEN", - "value" : "$(LIVEKIT_TOKEN)" + "key" : "LIVEKIT_TESTING_API_KEY", + "value" : "$(LIVEKIT_TESTING_API_KEY)" + }, + { + "key" : "LIVEKIT_TESTING_API_SECRET", + "value" : "$(LIVEKIT_TESTING_API_SECRET)" } ], "targetForVariableExpansion" : { diff --git a/Package.swift b/Package.swift index de85225df..7ea53752b 100644 --- a/Package.swift +++ b/Package.swift @@ -20,8 +20,11 @@ let package = Package( // LK-Prefixed Dynamic WebRTC XCFramework .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "114.5735.13"), .package(url: "https://github.com/apple/swift-protobuf.git", .upToNextMajor(from: "1.25.2")), - .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.5.3")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.5.4")), + // Only used for DocC generation + .package(url: "https://github.com/apple/swift-docc-plugin", .upToNextMajor(from: "1.3.0")), + // Only used for Testing + .package(url: "https://github.com/vapor/jwt-kit.git", .upToNextMajor(from: "4.13.2")), ], targets: [ .systemLibrary(name: "CHeaders"), @@ -37,11 +40,17 @@ let package = Package( ), .testTarget( name: "LiveKitTests", - dependencies: ["LiveKit"] + dependencies: [ + "LiveKit", + .product(name: "JWTKit", package: "jwt-kit"), + ] ), .testTarget( name: "LiveKitTestsObjC", - dependencies: ["LiveKit"] + dependencies: [ + "LiveKit", + .product(name: "JWTKit", package: "jwt-kit"), + ] ), ] ) diff --git a/Sources/LiveKit/Core/Transport.swift b/Sources/LiveKit/Core/Transport.swift index 9a430c9a9..b4cd6ffe0 100644 --- a/Sources/LiveKit/Core/Transport.swift +++ b/Sources/LiveKit/Core/Transport.swift @@ -15,7 +15,6 @@ */ import Foundation -import SwiftProtobuf @_implementationOnly import LiveKitWebRTC diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 11ed2e3f8..b729a4d1f 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -21,20 +21,28 @@ class PublishTests: XCTestCase { let room = Room() override func setUp() async throws { - let url = ProcessInfo.processInfo.environment["LIVEKIT_URL"] - let token = ProcessInfo.processInfo.environment["LIVEKIT_TOKEN"] + let url = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_URL"] + let apiKey = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_KEY"] + let apiSecret = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_SECRET"] guard let url else { - XCTFail("LIVEKIT_URL is nil") + XCTFail("LIVEKIT_TESTING_URL is nil") return } - guard let token else { - XCTFail("LIVEKIT_TOKEN is nil") + guard let apiKey else { + XCTFail("LIVEKIT_TESTING_API_KEY is nil") return } - try await room.connect(url: url, token: token) + guard let apiSecret else { + XCTFail("LIVEKIT_TESTING_API_SECRET is nil") + return + } + + let g = TokenGenerator(apiKey: apiKey, apiSecret: apiSecret, identity: "test_publisher01") + + try await room.connect(url: url, token: g.sign()) } override func tearDown() async throws { diff --git a/Tests/LiveKitTests/Support/TokenGenerator.swift b/Tests/LiveKitTests/Support/TokenGenerator.swift new file mode 100644 index 000000000..8406233f8 --- /dev/null +++ b/Tests/LiveKitTests/Support/TokenGenerator.swift @@ -0,0 +1,136 @@ +/* + * Copyright 2024 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import JWTKit + +public struct VideoGrant: Codable, Equatable { + /** name of the room, must be set for admin or join permissions */ + let room: String? + /** permission to create a room */ + let roomCreate: Bool? + /** permission to join a room as a participant, room must be set */ + let roomJoin: Bool? + /** permission to list rooms */ + let roomList: Bool? + /** permission to start a recording */ + let roomRecord: Bool? + /** permission to control a specific room, room must be set */ + let roomAdmin: Bool? + + /** + * allow participant to publish. If neither canPublish or canSubscribe is set, + * both publish and subscribe are enabled + */ + let canPublish: Bool? + /** allow participant to subscribe to other tracks */ + let canSubscribe: Bool? + /** + * allow participants to publish data, defaults to true if not set + */ + let canPublishData: Bool? + /** participant isn't visible to others */ + let hidden: Bool? + /** participant is recording the room, when set, allows room to indicate it's being recorded */ + let recorder: Bool? + + init(room: String? = nil, + roomCreate: Bool? = nil, + roomJoin: Bool? = nil, + roomList: Bool? = nil, + roomRecord: Bool? = nil, + roomAdmin: Bool? = nil, + canPublish: Bool? = nil, + canSubscribe: Bool? = nil, + canPublishData: Bool? = nil, + hidden: Bool? = nil, + recorder: Bool? = nil) + { + self.room = room + self.roomCreate = roomCreate + self.roomJoin = roomJoin + self.roomList = roomList + self.roomRecord = roomRecord + self.roomAdmin = roomAdmin + self.canPublish = canPublish + self.canSubscribe = canSubscribe + self.canPublishData = canPublishData + self.hidden = hidden + self.recorder = recorder + } +} + +public class TokenGenerator { + private struct Payload: JWTPayload, Equatable { + let exp: ExpirationClaim + let iss: IssuerClaim + let nbf: NotBeforeClaim + let sub: SubjectClaim + + let name: String? + let metadata: String? + let video: VideoGrant? + + func verify(using _: JWTSigner) throws { + fatalError("not implemented") + } + } + + // 30 mins + static let defaultTTL: TimeInterval = 30 * 60 + + // MARK: - Public + + public var apiKey: String + public var apiSecret: String + public var identity: String + public var ttl: TimeInterval + public var name: String? + public var metadata: String? + public var videoGrant: VideoGrant? + + // MARK: - Private + + private let signers = JWTSigners() + + init(apiKey: String, + apiSecret: String, + identity: String, + ttl: TimeInterval = defaultTTL) + { + self.apiKey = apiKey + self.apiSecret = apiSecret + self.identity = identity + self.ttl = ttl + } + + func sign() throws -> String { + // Add HMAC with SHA-256 signer. + signers.use(.hs256(key: apiSecret)) + + let n = Date().timeIntervalSince1970 + + let p = Payload(exp: .init(value: Date(timeIntervalSince1970: floor(n + ttl))), + iss: .init(stringLiteral: apiKey), + nbf: .init(value: Date(timeIntervalSince1970: floor(n))), + sub: .init(stringLiteral: identity), + name: name, + metadata: metadata, + video: videoGrant) + + return try signers.sign(p) + } +} diff --git a/Tests/LiveKitTestsObjC/Basic.m b/Tests/LiveKitTestsObjC/Basic.m index 05aa25532..34dd1e6d4 100644 --- a/Tests/LiveKitTestsObjC/Basic.m +++ b/Tests/LiveKitTestsObjC/Basic.m @@ -22,7 +22,7 @@ @interface Basic : XCTestCase @implementation Basic -- (void)sdkVersion { +- (void)testSdkVersion { NSLog(@"%@", LiveKitSDK.sdkVersion); } From 69d99f06b8dfc0c413bff4221805a8914487ea1f Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:45:58 +0900 Subject: [PATCH 04/18] add grants --- Tests/LiveKitTests/PublishTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index b729a4d1f..aaf61523d 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -42,6 +42,10 @@ class PublishTests: XCTestCase { let g = TokenGenerator(apiKey: apiKey, apiSecret: apiSecret, identity: "test_publisher01") + g.videoGrant = VideoGrant(room: "swiftsdk_test_01", + roomJoin: true, + canPublish: true) + try await room.connect(url: url, token: g.sign()) } From b69345842fb895717042a50cef5d9608a613a224 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 3 Mar 2024 03:06:34 +0900 Subject: [PATCH 05/18] Update testing-matrix.yaml --- .github/workflows/testing-matrix.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing-matrix.yaml b/.github/workflows/testing-matrix.yaml index 3c12e1630..50b9760ea 100644 --- a/.github/workflows/testing-matrix.yaml +++ b/.github/workflows/testing-matrix.yaml @@ -59,6 +59,6 @@ jobs: LIVEKIT_TESTING_API_SECRET: ${{ secrets.LIVEKIT_TESTING_API_SECRET }} run: | xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' \ - LIVEKIT_TESTING_URL='$LIVEKIT_TESTING_URL' \ - LIVEKIT_TESTING_API_KEY='$LIVEKIT_TESTING_API_KEY' \ - LIVEKIT_TESTING_API_SECRET='$LIVEKIT_TESTING_API_SECRET' + TEST_RUNNER_LIVEKIT_TESTING_URL='$LIVEKIT_TESTING_URL' \ + TEST_RUNNER_LIVEKIT_TESTING_API_KEY='$LIVEKIT_TESTING_API_KEY' \ + TEST_RUNNER_LIVEKIT_TESTING_API_SECRET='$LIVEKIT_TESTING_API_SECRET' From 5003f3c80581ff38064a658d89237a736487238b Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 3 Mar 2024 03:12:13 +0900 Subject: [PATCH 06/18] quiet --- .github/workflows/testing-matrix.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing-matrix.yaml b/.github/workflows/testing-matrix.yaml index 50b9760ea..9c2416517 100644 --- a/.github/workflows/testing-matrix.yaml +++ b/.github/workflows/testing-matrix.yaml @@ -58,7 +58,7 @@ jobs: LIVEKIT_TESTING_API_KEY: ${{ secrets.LIVEKIT_TESTING_API_KEY }} LIVEKIT_TESTING_API_SECRET: ${{ secrets.LIVEKIT_TESTING_API_SECRET }} run: | - xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' \ + xcodebuild test -quiet -scheme LiveKit -destination '${{ matrix.destination }}' \ TEST_RUNNER_LIVEKIT_TESTING_URL='$LIVEKIT_TESTING_URL' \ TEST_RUNNER_LIVEKIT_TESTING_API_KEY='$LIVEKIT_TESTING_API_KEY' \ TEST_RUNNER_LIVEKIT_TESTING_API_SECRET='$LIVEKIT_TESTING_API_SECRET' From 3b72a70607b7c9decfef7e4db171007b349a9fb6 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:57:51 +0900 Subject: [PATCH 07/18] ref --- Tests/LiveKitTests/PublishTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index aaf61523d..9e45ab15c 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -40,13 +40,13 @@ class PublishTests: XCTestCase { return } - let g = TokenGenerator(apiKey: apiKey, apiSecret: apiSecret, identity: "test_publisher01") + let tokenGenerator = TokenGenerator(apiKey: apiKey, apiSecret: apiSecret, identity: "test_publisher01") - g.videoGrant = VideoGrant(room: "swiftsdk_test_01", - roomJoin: true, - canPublish: true) + tokenGenerator.videoGrant = VideoGrant(room: "swiftsdk_test_01", + roomJoin: true, + canPublish: true) - try await room.connect(url: url, token: g.sign()) + try await room.connect(url: url, token: tokenGenerator.sign()) } override func tearDown() async throws { From ced34670351b825f79f8b552b705fb6539d0a9ef Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:20:49 +0900 Subject: [PATCH 08/18] disable publish tests for ios --- Tests/LiveKitTests/PublishTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 9e45ab15c..1da6dd70d 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -17,6 +17,8 @@ @testable import LiveKit import XCTest +#if !os(iOS) +// TODO: Make this work with iOS class PublishTests: XCTestCase { let room = Room() @@ -71,3 +73,4 @@ class PublishTests: XCTestCase { print("Stats: \(String(describing: stats))") } } +#endif From cf88f8da93e5a69f548faa42891c06b4c42b1bca Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:36:38 +0900 Subject: [PATCH 09/18] enable for ios but not ios simulator --- Tests/LiveKitTests/PublishTests.swift | 5 +++-- Tests/LiveKitTestsObjC/Basic.m | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 1da6dd70d..d00e51874 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -17,8 +17,9 @@ @testable import LiveKit import XCTest -#if !os(iOS) -// TODO: Make this work with iOS +#if !targetEnvironment(simulator) +// TODO: Make this work with iOS Simulator + class PublishTests: XCTestCase { let room = Room() diff --git a/Tests/LiveKitTestsObjC/Basic.m b/Tests/LiveKitTestsObjC/Basic.m index 34dd1e6d4..c286b5dee 100644 --- a/Tests/LiveKitTestsObjC/Basic.m +++ b/Tests/LiveKitTestsObjC/Basic.m @@ -14,9 +14,10 @@ * limitations under the License. */ -#import +@import XCTest; @import LiveKit; +// Simple ObjC test just to ensure ObjC SDK code compiles. @interface Basic : XCTestCase @end From 14263e597ab41771d379f9eb8615dd893446c3cf Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:04:40 +0900 Subject: [PATCH 10/18] enable local testing --- .github/workflows/testing-matrix.yaml | 16 +++++++--------- Tests/LiveKitTests/PublishTests.swift | 21 +++------------------ 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/.github/workflows/testing-matrix.yaml b/.github/workflows/testing-matrix.yaml index 9c2416517..b9f7e53c9 100644 --- a/.github/workflows/testing-matrix.yaml +++ b/.github/workflows/testing-matrix.yaml @@ -29,6 +29,12 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install LiveKit Server + run: brew install livekit + + - name: Run LiveKit Server + run: livekit-server --dev & + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ matrix.xcode-version }} @@ -53,12 +59,4 @@ jobs: run: xcodebuild -scheme LiveKit -showdestinations - name: Run All Tests - env: - LIVEKIT_TESTING_URL: ${{ secrets.LIVEKIT_TESTING_URL }} - LIVEKIT_TESTING_API_KEY: ${{ secrets.LIVEKIT_TESTING_API_KEY }} - LIVEKIT_TESTING_API_SECRET: ${{ secrets.LIVEKIT_TESTING_API_SECRET }} - run: | - xcodebuild test -quiet -scheme LiveKit -destination '${{ matrix.destination }}' \ - TEST_RUNNER_LIVEKIT_TESTING_URL='$LIVEKIT_TESTING_URL' \ - TEST_RUNNER_LIVEKIT_TESTING_API_KEY='$LIVEKIT_TESTING_API_KEY' \ - TEST_RUNNER_LIVEKIT_TESTING_API_SECRET='$LIVEKIT_TESTING_API_SECRET' + run: xcodebuild test -quiet -scheme LiveKit -destination '${{ matrix.destination }}' diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index d00e51874..5c42d3580 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -24,24 +24,9 @@ class PublishTests: XCTestCase { let room = Room() override func setUp() async throws { - let url = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_URL"] - let apiKey = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_KEY"] - let apiSecret = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_SECRET"] - - guard let url else { - XCTFail("LIVEKIT_TESTING_URL is nil") - return - } - - guard let apiKey else { - XCTFail("LIVEKIT_TESTING_API_KEY is nil") - return - } - - guard let apiSecret else { - XCTFail("LIVEKIT_TESTING_API_SECRET is nil") - return - } + let url = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_URL"] ?? "ws://localhost:7880" + let apiKey = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_KEY"] ?? "devkey" + let apiSecret = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_SECRET"] ?? "secret" let tokenGenerator = TokenGenerator(apiKey: apiKey, apiSecret: apiSecret, identity: "test_publisher01") From 4006c1798a16f15d9173b1d81ab9d8ab887b4be8 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:05:10 +0900 Subject: [PATCH 11/18] Update PublishTests.swift --- Tests/LiveKitTests/PublishTests.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 5c42d3580..5494bd7c3 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -17,9 +17,6 @@ @testable import LiveKit import XCTest -#if !targetEnvironment(simulator) -// TODO: Make this work with iOS Simulator - class PublishTests: XCTestCase { let room = Room() @@ -59,4 +56,3 @@ class PublishTests: XCTestCase { print("Stats: \(String(describing: stats))") } } -#endif From 04cecfcda52233467fb50ab8b1294bdff59b25b7 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:42:20 +0900 Subject: [PATCH 12/18] 2 participants --- .github/workflows/testing-matrix.yaml | 2 +- Tests/LiveKitTests/PublishTests.swift | 56 +++++++++++++------ .../LiveKitTests/Support/TokenGenerator.swift | 18 ++++++ 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/.github/workflows/testing-matrix.yaml b/.github/workflows/testing-matrix.yaml index b9f7e53c9..b5c289523 100644 --- a/.github/workflows/testing-matrix.yaml +++ b/.github/workflows/testing-matrix.yaml @@ -59,4 +59,4 @@ jobs: run: xcodebuild -scheme LiveKit -showdestinations - name: Run All Tests - run: xcodebuild test -quiet -scheme LiveKit -destination '${{ matrix.destination }}' + run: xcodebuild test -scheme LiveKit -destination '${{ matrix.destination }}' diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 5494bd7c3..b2108f2f5 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -14,45 +14,67 @@ * limitations under the License. */ +import Combine @testable import LiveKit import XCTest class PublishTests: XCTestCase { - let room = Room() + let room1 = Room() + let room2 = Room() + + var watchRoom1: AnyCancellable? + var watchRoom2: AnyCancellable? override func setUp() async throws { - let url = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_URL"] ?? "ws://localhost:7880" - let apiKey = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_KEY"] ?? "devkey" - let apiSecret = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_SECRET"] ?? "secret" + let url = testUrl() + + let token1 = try testToken(for: "room01", identity: "identity01") + try await room1.connect(url: url, token: token1) + + let token2 = try testToken(for: "room01", identity: "identity02") + try await room2.connect(url: url, token: token2) - let tokenGenerator = TokenGenerator(apiKey: apiKey, apiSecret: apiSecret, identity: "test_publisher01") + let room1ParticipantCountIs2 = expectation(description: "Room1 Participant count is 2") + room1ParticipantCountIs2.assertForOverFulfill = false - tokenGenerator.videoGrant = VideoGrant(room: "swiftsdk_test_01", - roomJoin: true, - canPublish: true) + let room2ParticipantCountIs2 = expectation(description: "Room2 Participant count is 2") + room2ParticipantCountIs2.assertForOverFulfill = false - try await room.connect(url: url, token: tokenGenerator.sign()) + watchRoom1 = room1.objectWillChange.sink { _ in + if self.room1.participantCount == 2 { + room1ParticipantCountIs2.fulfill() + } + } + + watchRoom2 = room2.objectWillChange.sink { _ in + if self.room2.participantCount == 2 { + room2ParticipantCountIs2.fulfill() + } + } + + // Wait until both room's participant count is 2 + await fulfillment(of: [room1ParticipantCountIs2, room2ParticipantCountIs2], timeout: 10) } override func tearDown() async throws { - await room.disconnect() + await room1.disconnect() + await room2.disconnect() + watchRoom1?.cancel() + watchRoom2?.cancel() } func testResolveSid() async throws { - XCTAssert(room.connectionState == .connected) + XCTAssert(room1.connectionState == .connected) - let sid = try await room.sid() + let sid = try await room1.sid() print("Room.sid(): \(String(describing: sid))") XCTAssert(sid.stringValue.starts(with: "RM_")) } func testPublishMic() async throws { - XCTAssert(room.connectionState == .connected) + XCTAssert(room1.connectionState == .connected) - try await room.localParticipant.setMicrophone(enabled: true) + try await room1.localParticipant.setMicrophone(enabled: true) sleep(5) - - let stats = room.localParticipant.localAudioTracks.first?.track?.statistics - print("Stats: \(String(describing: stats))") } } diff --git a/Tests/LiveKitTests/Support/TokenGenerator.swift b/Tests/LiveKitTests/Support/TokenGenerator.swift index 8406233f8..7f8d9fdf0 100644 --- a/Tests/LiveKitTests/Support/TokenGenerator.swift +++ b/Tests/LiveKitTests/Support/TokenGenerator.swift @@ -17,6 +17,24 @@ import Foundation import JWTKit +public func testUrl() -> String { + ProcessInfo.processInfo.environment["LIVEKIT_TESTING_URL"] ?? "ws://localhost:7880" +} + +public func testToken(for room: String, identity: String) throws -> String { + let apiKey = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_KEY"] ?? "devkey" + let apiSecret = ProcessInfo.processInfo.environment["LIVEKIT_TESTING_API_SECRET"] ?? "secret" + + let tokenGenerator = TokenGenerator(apiKey: apiKey, + apiSecret: apiSecret, + identity: identity) + + tokenGenerator.videoGrant = VideoGrant(room: room, + roomJoin: true, + canPublish: true) + return try tokenGenerator.sign() +} + public struct VideoGrant: Codable, Equatable { /** name of the room, must be set for admin or join permissions */ let room: String? From e620b4c11168f7def86857860dac96c919c357bb Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:02:34 +0900 Subject: [PATCH 13/18] Create Xcode14.2Backport.swift --- .../Support/Xcode14.2Backport.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Tests/LiveKitTests/Support/Xcode14.2Backport.swift diff --git a/Tests/LiveKitTests/Support/Xcode14.2Backport.swift b/Tests/LiveKitTests/Support/Xcode14.2Backport.swift new file mode 100644 index 000000000..acf1e07ee --- /dev/null +++ b/Tests/LiveKitTests/Support/Xcode14.2Backport.swift @@ -0,0 +1,35 @@ +/* + * Copyright 2024 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import XCTest + +/// Support for Xcode 14.2 +#if !swift(>=5.7) +extension XCTestCase { + func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false) async { + await withCheckedContinuation { continuation in + // This function operates by blocking a background thread instead of one owned by libdispatch or by the + // Swift runtime (as used by Swift concurrency.) To ensure we use a thread owned by neither subsystem, use + // Foundation's Thread.detachNewThread(_:). + Thread.detachNewThread { [self] in + wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder) + continuation.resume() + } + } + } +} +#endif From 7bbfc0728b5c9ab808c953ac86c283c1d3c4f4bc Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:19:56 +0900 Subject: [PATCH 14/18] concurrent mic publish test --- Tests/LiveKitTests/PublishTests.swift | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index b2108f2f5..016f4aa94 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -71,10 +71,34 @@ class PublishTests: XCTestCase { XCTAssert(sid.stringValue.starts(with: "RM_")) } - func testPublishMic() async throws { - XCTAssert(room1.connectionState == .connected) + func testConcurrentMicPublish() async throws { + // Lock + struct State { + var firstMicPublication: LocalTrackPublication? + } - try await room1.localParticipant.setMicrophone(enabled: true) - sleep(5) + let _state = StateSync(State()) + + // Run Tasks concurrently + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 1 ... 100 { + group.addTask { + let result = try await self.room1.localParticipant.setMicrophone(enabled: true) + + if let result { + _state.mutate { + if let firstMicPublication = $0.firstMicPublication { + XCTAssert(result == firstMicPublication, "Duplicate mic track has been published") + } else { + $0.firstMicPublication = result + print("Did publish first mic track: \(String(describing: result))") + } + } + } + } + } + + try await group.waitForAll() + } } } From 064b1cd20de4b3a69104d63443f1005903722d82 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:25:40 +0900 Subject: [PATCH 15/18] fix backport ver --- Tests/LiveKitTests/Support/Xcode14.2Backport.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/LiveKitTests/Support/Xcode14.2Backport.swift b/Tests/LiveKitTests/Support/Xcode14.2Backport.swift index acf1e07ee..64145a575 100644 --- a/Tests/LiveKitTests/Support/Xcode14.2Backport.swift +++ b/Tests/LiveKitTests/Support/Xcode14.2Backport.swift @@ -18,7 +18,7 @@ import Foundation import XCTest /// Support for Xcode 14.2 -#if !swift(>=5.7) +#if !compiler(>=5.8) extension XCTestCase { func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false) async { await withCheckedContinuation { continuation in From 88f59b239d8a67c809fa49a873b8f8e6c94dd211 Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:51:54 +0900 Subject: [PATCH 16/18] update timeout --- Tests/LiveKitTests/PublishTests.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 016f4aa94..a8bdf2951 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -25,6 +25,7 @@ class PublishTests: XCTestCase { var watchRoom1: AnyCancellable? var watchRoom2: AnyCancellable? + @MainActor override func setUp() async throws { let url = testUrl() @@ -41,19 +42,23 @@ class PublishTests: XCTestCase { room2ParticipantCountIs2.assertForOverFulfill = false watchRoom1 = room1.objectWillChange.sink { _ in - if self.room1.participantCount == 2 { - room1ParticipantCountIs2.fulfill() + DispatchQueue.main.async { + if self.room1.participantCount == 2 { + room1ParticipantCountIs2.fulfill() + } } } watchRoom2 = room2.objectWillChange.sink { _ in - if self.room2.participantCount == 2 { - room2ParticipantCountIs2.fulfill() + DispatchQueue.main.async { + if self.room2.participantCount == 2 { + room2ParticipantCountIs2.fulfill() + } } } // Wait until both room's participant count is 2 - await fulfillment(of: [room1ParticipantCountIs2, room2ParticipantCountIs2], timeout: 10) + await fulfillment(of: [room1ParticipantCountIs2, room2ParticipantCountIs2], timeout: 30) } override func tearDown() async throws { From 0c321fd95b592180afcf4b89cfa14f9c4aeeaf2c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:11:58 +0900 Subject: [PATCH 17/18] Update PublishTests.swift --- Tests/LiveKitTests/PublishTests.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index a8bdf2951..40c52241d 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -25,7 +25,6 @@ class PublishTests: XCTestCase { var watchRoom1: AnyCancellable? var watchRoom2: AnyCancellable? - @MainActor override func setUp() async throws { let url = testUrl() @@ -42,18 +41,14 @@ class PublishTests: XCTestCase { room2ParticipantCountIs2.assertForOverFulfill = false watchRoom1 = room1.objectWillChange.sink { _ in - DispatchQueue.main.async { - if self.room1.participantCount == 2 { - room1ParticipantCountIs2.fulfill() - } + if self.room1.allParticipants.count == 2 { + room1ParticipantCountIs2.fulfill() } } watchRoom2 = room2.objectWillChange.sink { _ in - DispatchQueue.main.async { - if self.room2.participantCount == 2 { - room2ParticipantCountIs2.fulfill() - } + if self.room2.allParticipants.count == 2 { + room2ParticipantCountIs2.fulfill() } } From b1cc3268228064b6249d2bbf618c751e1d4455fb Mon Sep 17 00:00:00 2001 From: hiroshihorie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 7 Mar 2024 22:31:14 +0900 Subject: [PATCH 18/18] clean up --- Tests/LiveKitTests/PublishTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/LiveKitTests/PublishTests.swift b/Tests/LiveKitTests/PublishTests.swift index 40c52241d..36cf15a5c 100644 --- a/Tests/LiveKitTests/PublishTests.swift +++ b/Tests/LiveKitTests/PublishTests.swift @@ -15,6 +15,7 @@ */ import Combine +import CoreMedia @testable import LiveKit import XCTest @@ -100,5 +101,8 @@ class PublishTests: XCTestCase { try await group.waitForAll() } + + // Reset + await room1.localParticipant.unpublishAll() } }