davidliu
Committed by GitHub

mock e2e tests (#19)

* mock e2e tests

* fix peer connection mock initialization
正在显示 18 个修改的文件 包含 653 行增加163 行删除
@@ -43,7 +43,7 @@ android { @@ -43,7 +43,7 @@ android {
43 targetCompatibility java_version 43 targetCompatibility java_version
44 } 44 }
45 kotlinOptions { 45 kotlinOptions {
46 - freeCompilerArgs = ["-Xinline-classes"] 46 + freeCompilerArgs = ["-Xinline-classes", "-Xopt-in=kotlin.RequiresOptIn"]
47 jvmTarget = java_version 47 jvmTarget = java_version
48 } 48 }
49 } 49 }
@@ -117,6 +117,7 @@ dependencies { @@ -117,6 +117,7 @@ dependencies {
117 testImplementation "org.mockito.kotlin:mockito-kotlin:3.1.0" 117 testImplementation "org.mockito.kotlin:mockito-kotlin:3.1.0"
118 testImplementation 'androidx.test:core:1.4.0' 118 testImplementation 'androidx.test:core:1.4.0'
119 testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3" 119 testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3"
  120 + kaptTest 'com.google.dagger:dagger-compiler:2.38'
120 androidTestImplementation 'androidx.test.ext:junit:1.1.3' 121 androidTestImplementation 'androidx.test.ext:junit:1.1.3'
121 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 122 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
122 } 123 }
@@ -8,27 +8,25 @@ import kotlinx.serialization.json.Json @@ -8,27 +8,25 @@ import kotlinx.serialization.json.Json
8 import javax.inject.Named 8 import javax.inject.Named
9 9
10 @Module 10 @Module
11 -class JsonFormatModule {  
12 - companion object {  
13 - @Provides  
14 - fun protobufJsonFormatParser(): JsonFormat.Parser {  
15 - return JsonFormat.parser()  
16 - } 11 +object JsonFormatModule {
  12 + @Provides
  13 + fun protobufJsonFormatParser(): JsonFormat.Parser {
  14 + return JsonFormat.parser()
  15 + }
17 16
18 - @Provides  
19 - fun protobufJsonFormatPrinter(): JsonFormat.Printer {  
20 - return JsonFormat.printer()  
21 - } 17 + @Provides
  18 + fun protobufJsonFormatPrinter(): JsonFormat.Printer {
  19 + return JsonFormat.printer()
  20 + }
22 21
23 - @Provides  
24 - @Reusable  
25 - fun kotlinSerializationJson(): Json =  
26 - Json {  
27 - ignoreUnknownKeys = true  
28 - } 22 + @Provides
  23 + @Reusable
  24 + fun kotlinSerializationJson(): Json =
  25 + Json {
  26 + ignoreUnknownKeys = true
  27 + }
29 28
30 - @Provides  
31 - @Named(InjectionNames.SIGNAL_JSON_ENABLED)  
32 - fun signalJsonEnabled(): Boolean = false  
33 - } 29 + @Provides
  30 + @Named(InjectionNames.SIGNAL_JSON_ENABLED)
  31 + fun signalJsonEnabled(): Boolean = false
34 } 32 }
@@ -13,143 +13,141 @@ import javax.inject.Singleton @@ -13,143 +13,141 @@ import javax.inject.Singleton
13 13
14 14
15 @Module 15 @Module
16 -class RTCModule {  
17 - companion object {  
18 - @Provides  
19 - @Singleton  
20 - fun audioModule(appContext: Context): AudioDeviceModule {  
21 -  
22 - // Set audio record error callbacks.  
23 - val audioRecordErrorCallback = object : JavaAudioDeviceModule.AudioRecordErrorCallback {  
24 - override fun onWebRtcAudioRecordInitError(errorMessage: String?) {  
25 - LKLog.e { "onWebRtcAudioRecordInitError: $errorMessage" }  
26 - }  
27 -  
28 - override fun onWebRtcAudioRecordStartError(  
29 - errorCode: JavaAudioDeviceModule.AudioRecordStartErrorCode?,  
30 - errorMessage: String?  
31 - ) {  
32 - LKLog.e { "onWebRtcAudioRecordStartError: $errorCode. $errorMessage" }  
33 - }  
34 -  
35 - override fun onWebRtcAudioRecordError(errorMessage: String?) {  
36 - LKLog.e { "onWebRtcAudioRecordError: $errorMessage" }  
37 - } 16 +object RTCModule {
  17 + @Provides
  18 + @Singleton
  19 + fun audioModule(appContext: Context): AudioDeviceModule {
  20 +
  21 + // Set audio record error callbacks.
  22 + val audioRecordErrorCallback = object : JavaAudioDeviceModule.AudioRecordErrorCallback {
  23 + override fun onWebRtcAudioRecordInitError(errorMessage: String?) {
  24 + LKLog.e { "onWebRtcAudioRecordInitError: $errorMessage" }
38 } 25 }
39 26
40 - val audioTrackErrorCallback = object : JavaAudioDeviceModule.AudioTrackErrorCallback {  
41 - override fun onWebRtcAudioTrackInitError(errorMessage: String?) {  
42 - LKLog.e { "onWebRtcAudioTrackInitError: $errorMessage" }  
43 - }  
44 -  
45 - override fun onWebRtcAudioTrackStartError(  
46 - errorCode: JavaAudioDeviceModule.AudioTrackStartErrorCode?,  
47 - errorMessage: String?  
48 - ) {  
49 - LKLog.e { "onWebRtcAudioTrackStartError: $errorCode. $errorMessage" }  
50 - }  
51 -  
52 - override fun onWebRtcAudioTrackError(errorMessage: String?) {  
53 - LKLog.e { "onWebRtcAudioTrackError: $errorMessage" }  
54 - } 27 + override fun onWebRtcAudioRecordStartError(
  28 + errorCode: JavaAudioDeviceModule.AudioRecordStartErrorCode?,
  29 + errorMessage: String?
  30 + ) {
  31 + LKLog.e { "onWebRtcAudioRecordStartError: $errorCode. $errorMessage" }
  32 + }
55 33
  34 + override fun onWebRtcAudioRecordError(errorMessage: String?) {
  35 + LKLog.e { "onWebRtcAudioRecordError: $errorMessage" }
56 } 36 }
57 - val audioRecordStateCallback: JavaAudioDeviceModule.AudioRecordStateCallback = object :  
58 - JavaAudioDeviceModule.AudioRecordStateCallback {  
59 - override fun onWebRtcAudioRecordStart() {  
60 - LKLog.i { "Audio recording starts" }  
61 - }  
62 -  
63 - override fun onWebRtcAudioRecordStop() {  
64 - LKLog.i { "Audio recording stops" }  
65 - } 37 + }
  38 +
  39 + val audioTrackErrorCallback = object : JavaAudioDeviceModule.AudioTrackErrorCallback {
  40 + override fun onWebRtcAudioTrackInitError(errorMessage: String?) {
  41 + LKLog.e { "onWebRtcAudioTrackInitError: $errorMessage" }
66 } 42 }
67 43
68 - // Set audio track state callbacks.  
69 - val audioTrackStateCallback: JavaAudioDeviceModule.AudioTrackStateCallback = object :  
70 - JavaAudioDeviceModule.AudioTrackStateCallback {  
71 - override fun onWebRtcAudioTrackStart() {  
72 - LKLog.i { "Audio playout starts" }  
73 - } 44 + override fun onWebRtcAudioTrackStartError(
  45 + errorCode: JavaAudioDeviceModule.AudioTrackStartErrorCode?,
  46 + errorMessage: String?
  47 + ) {
  48 + LKLog.e { "onWebRtcAudioTrackStartError: $errorCode. $errorMessage" }
  49 + }
74 50
75 - override fun onWebRtcAudioTrackStop() {  
76 - LKLog.i { "Audio playout stops" }  
77 - } 51 + override fun onWebRtcAudioTrackError(errorMessage: String?) {
  52 + LKLog.e { "onWebRtcAudioTrackError: $errorMessage" }
78 } 53 }
79 54
80 - return JavaAudioDeviceModule.builder(appContext)  
81 - .setUseHardwareAcousticEchoCanceler(true)  
82 - .setUseHardwareNoiseSuppressor(true)  
83 - .setAudioRecordErrorCallback(audioRecordErrorCallback)  
84 - .setAudioTrackErrorCallback(audioTrackErrorCallback)  
85 - .setAudioRecordStateCallback(audioRecordStateCallback)  
86 - .setAudioTrackStateCallback(audioTrackStateCallback)  
87 - .createAudioDeviceModule()  
88 } 55 }
  56 + val audioRecordStateCallback: JavaAudioDeviceModule.AudioRecordStateCallback = object :
  57 + JavaAudioDeviceModule.AudioRecordStateCallback {
  58 + override fun onWebRtcAudioRecordStart() {
  59 + LKLog.i { "Audio recording starts" }
  60 + }
89 61
90 - @Provides  
91 - @Singleton  
92 - fun eglBase(): EglBase {  
93 - return EglBase.create() 62 + override fun onWebRtcAudioRecordStop() {
  63 + LKLog.i { "Audio recording stops" }
  64 + }
94 } 65 }
95 66
96 - @Provides  
97 - fun eglContext(eglBase: EglBase): EglBase.Context = eglBase.eglBaseContext  
98 -  
99 - @Provides  
100 - fun videoEncoderFactory(  
101 - @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)  
102 - videoHwAccel: Boolean,  
103 - eglContext: EglBase.Context  
104 - ): VideoEncoderFactory {  
105 -  
106 - return if (videoHwAccel) {  
107 - SimulcastVideoEncoderFactoryWrapper(  
108 - eglContext,  
109 - enableIntelVp8Encoder = true,  
110 - enableH264HighProfile = false,  
111 - )  
112 - } else {  
113 - SoftwareVideoEncoderFactory() 67 + // Set audio track state callbacks.
  68 + val audioTrackStateCallback: JavaAudioDeviceModule.AudioTrackStateCallback = object :
  69 + JavaAudioDeviceModule.AudioTrackStateCallback {
  70 + override fun onWebRtcAudioTrackStart() {
  71 + LKLog.i { "Audio playout starts" }
114 } 72 }
115 - }  
116 73
117 - @Provides  
118 - fun videoDecoderFactory(  
119 - @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)  
120 - videoHwAccel: Boolean,  
121 - eglContext: EglBase.Context,  
122 - ): VideoDecoderFactory {  
123 - return if (videoHwAccel) {  
124 - DefaultVideoDecoderFactory(eglContext)  
125 - } else {  
126 - SoftwareVideoDecoderFactory() 74 + override fun onWebRtcAudioTrackStop() {
  75 + LKLog.i { "Audio playout stops" }
127 } 76 }
128 } 77 }
129 78
130 - @Provides  
131 - @Singleton  
132 - fun peerConnectionFactory(  
133 - appContext: Context,  
134 - audioDeviceModule: AudioDeviceModule,  
135 - videoEncoderFactory: VideoEncoderFactory,  
136 - videoDecoderFactory: VideoDecoderFactory,  
137 - ): PeerConnectionFactory {  
138 - PeerConnectionFactory.initialize(  
139 - PeerConnectionFactory.InitializationOptions  
140 - .builder(appContext)  
141 - .createInitializationOptions()  
142 - ) 79 + return JavaAudioDeviceModule.builder(appContext)
  80 + .setUseHardwareAcousticEchoCanceler(true)
  81 + .setUseHardwareNoiseSuppressor(true)
  82 + .setAudioRecordErrorCallback(audioRecordErrorCallback)
  83 + .setAudioTrackErrorCallback(audioTrackErrorCallback)
  84 + .setAudioRecordStateCallback(audioRecordStateCallback)
  85 + .setAudioTrackStateCallback(audioTrackStateCallback)
  86 + .createAudioDeviceModule()
  87 + }
143 88
144 - return PeerConnectionFactory.builder()  
145 - .setAudioDeviceModule(audioDeviceModule)  
146 - .setVideoEncoderFactory(videoEncoderFactory)  
147 - .setVideoDecoderFactory(videoDecoderFactory)  
148 - .createPeerConnectionFactory() 89 + @Provides
  90 + @Singleton
  91 + fun eglBase(): EglBase {
  92 + return EglBase.create()
  93 + }
  94 +
  95 + @Provides
  96 + fun eglContext(eglBase: EglBase): EglBase.Context = eglBase.eglBaseContext
  97 +
  98 + @Provides
  99 + fun videoEncoderFactory(
  100 + @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)
  101 + videoHwAccel: Boolean,
  102 + eglContext: EglBase.Context
  103 + ): VideoEncoderFactory {
  104 +
  105 + return if (videoHwAccel) {
  106 + SimulcastVideoEncoderFactoryWrapper(
  107 + eglContext,
  108 + enableIntelVp8Encoder = true,
  109 + enableH264HighProfile = false,
  110 + )
  111 + } else {
  112 + SoftwareVideoEncoderFactory()
149 } 113 }
  114 + }
150 115
151 - @Provides 116 + @Provides
  117 + fun videoDecoderFactory(
152 @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL) 118 @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)
153 - fun videoHwAccel() = true 119 + videoHwAccel: Boolean,
  120 + eglContext: EglBase.Context,
  121 + ): VideoDecoderFactory {
  122 + return if (videoHwAccel) {
  123 + DefaultVideoDecoderFactory(eglContext)
  124 + } else {
  125 + SoftwareVideoDecoderFactory()
  126 + }
154 } 127 }
  128 +
  129 + @Provides
  130 + @Singleton
  131 + fun peerConnectionFactory(
  132 + appContext: Context,
  133 + audioDeviceModule: AudioDeviceModule,
  134 + videoEncoderFactory: VideoEncoderFactory,
  135 + videoDecoderFactory: VideoDecoderFactory,
  136 + ): PeerConnectionFactory {
  137 + PeerConnectionFactory.initialize(
  138 + PeerConnectionFactory.InitializationOptions
  139 + .builder(appContext)
  140 + .createInitializationOptions()
  141 + )
  142 +
  143 + return PeerConnectionFactory.builder()
  144 + .setAudioDeviceModule(audioDeviceModule)
  145 + .setVideoEncoderFactory(videoEncoderFactory)
  146 + .setVideoDecoderFactory(videoDecoderFactory)
  147 + .createPeerConnectionFactory()
  148 + }
  149 +
  150 + @Provides
  151 + @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)
  152 + fun videoHwAccel() = true
155 } 153 }
@@ -7,17 +7,15 @@ import okhttp3.WebSocket @@ -7,17 +7,15 @@ import okhttp3.WebSocket
7 import javax.inject.Singleton 7 import javax.inject.Singleton
8 8
9 @Module 9 @Module
10 -class WebModule {  
11 - companion object {  
12 - @Provides  
13 - @Singleton  
14 - fun okHttpClient(): OkHttpClient {  
15 - return OkHttpClient()  
16 - } 10 +object WebModule {
  11 + @Provides
  12 + @Singleton
  13 + fun okHttpClient(): OkHttpClient {
  14 + return OkHttpClient()
  15 + }
17 16
18 - @Provides  
19 - fun websocketFactory(okHttpClient: OkHttpClient): WebSocket.Factory {  
20 - return okHttpClient  
21 - } 17 + @Provides
  18 + fun websocketFactory(okHttpClient: OkHttpClient): WebSocket.Factory {
  19 + return okHttpClient
22 } 20 }
23 } 21 }
  1 +package io.livekit.android.mock
  2 +
  3 +import org.webrtc.DataChannel
  4 +
  5 +class MockDataChannel(private val label: String?) : DataChannel(1L) {
  6 +
  7 + var observer: DataChannel.Observer? = null
  8 + override fun registerObserver(observer: Observer?) {
  9 + this.observer = observer
  10 + }
  11 +
  12 + override fun unregisterObserver() {
  13 + observer = null
  14 + }
  15 +
  16 + override fun label(): String? {
  17 + return label
  18 + }
  19 +
  20 + override fun id(): Int {
  21 + return 0
  22 + }
  23 +
  24 + override fun state(): State {
  25 + return State.OPEN
  26 + }
  27 +
  28 + override fun bufferedAmount(): Long {
  29 + return 0
  30 + }
  31 +
  32 + override fun send(buffer: Buffer?): Boolean {
  33 + return true
  34 + }
  35 +}
1 -package io.livekit.android.room.mock 1 +package io.livekit.android.mock
2 2
3 import android.graphics.SurfaceTexture 3 import android.graphics.SurfaceTexture
4 import android.view.Surface 4 import android.view.Surface
  1 +package io.livekit.android.mock
  2 +
  3 +import org.webrtc.*
  4 +
  5 +private class MockNativePeerConnectionFactory : NativePeerConnectionFactory {
  6 + override fun createNativePeerConnection(): Long = 0L
  7 +}
  8 +
  9 +class MockPeerConnection(
  10 + private val observer: PeerConnection.Observer?
  11 +) : PeerConnection(MockNativePeerConnectionFactory()) {
  12 +
  13 + var localDesc: SessionDescription? = null
  14 + var remoteDesc: SessionDescription? = null
  15 + override fun getLocalDescription(): SessionDescription? = localDesc
  16 + override fun setLocalDescription(observer: SdpObserver?, sdp: SessionDescription?) {
  17 + localDesc = sdp
  18 + observer?.onSetSuccess()
  19 + }
  20 +
  21 + override fun getRemoteDescription(): SessionDescription? = remoteDesc
  22 + override fun setRemoteDescription(observer: SdpObserver?, sdp: SessionDescription?) {
  23 + remoteDesc = sdp
  24 + observer?.onSetSuccess()
  25 + }
  26 +
  27 + override fun getCertificate(): RtcCertificatePem? {
  28 + return null
  29 + }
  30 +
  31 + override fun createDataChannel(label: String?, init: DataChannel.Init?): DataChannel {
  32 + return MockDataChannel(label)
  33 + }
  34 +
  35 + override fun createOffer(observer: SdpObserver?, constraints: MediaConstraints?) {
  36 + val sdp = SessionDescription(SessionDescription.Type.OFFER, "")
  37 + observer?.onCreateSuccess(sdp)
  38 + }
  39 +
  40 + override fun createAnswer(observer: SdpObserver?, constraints: MediaConstraints?) {
  41 + val sdp = SessionDescription(SessionDescription.Type.ANSWER, "")
  42 + observer?.onCreateSuccess(sdp)
  43 + }
  44 +
  45 +
  46 + override fun setAudioPlayout(playout: Boolean) {
  47 + }
  48 +
  49 + override fun setAudioRecording(recording: Boolean) {
  50 + }
  51 +
  52 + override fun setConfiguration(config: RTCConfiguration?): Boolean {
  53 + return true
  54 + }
  55 +
  56 + override fun addIceCandidate(candidate: IceCandidate?): Boolean {
  57 + return true
  58 + }
  59 +
  60 + override fun removeIceCandidates(candidates: Array<out IceCandidate>?): Boolean {
  61 + return true
  62 + }
  63 +
  64 + override fun addStream(stream: MediaStream?): Boolean {
  65 + return super.addStream(stream)
  66 + }
  67 +
  68 + override fun removeStream(stream: MediaStream?) {
  69 + super.removeStream(stream)
  70 + }
  71 +
  72 + override fun createSender(kind: String?, stream_id: String?): RtpSender {
  73 + return super.createSender(kind, stream_id)
  74 + }
  75 +
  76 + override fun getSenders(): MutableList<RtpSender> {
  77 + return super.getSenders()
  78 + }
  79 +
  80 + override fun getReceivers(): MutableList<RtpReceiver> {
  81 + return super.getReceivers()
  82 + }
  83 +
  84 + override fun getTransceivers(): MutableList<RtpTransceiver> {
  85 + return super.getTransceivers()
  86 + }
  87 +
  88 + override fun addTrack(track: MediaStreamTrack?): RtpSender {
  89 + return super.addTrack(track)
  90 + }
  91 +
  92 + override fun addTrack(track: MediaStreamTrack?, streamIds: MutableList<String>?): RtpSender {
  93 + return super.addTrack(track, streamIds)
  94 + }
  95 +
  96 + override fun removeTrack(sender: RtpSender?): Boolean {
  97 + return super.removeTrack(sender)
  98 + }
  99 +
  100 + override fun addTransceiver(track: MediaStreamTrack?): RtpTransceiver {
  101 + return super.addTransceiver(track)
  102 + }
  103 +
  104 + override fun addTransceiver(
  105 + track: MediaStreamTrack?,
  106 + init: RtpTransceiver.RtpTransceiverInit?
  107 + ): RtpTransceiver {
  108 + return super.addTransceiver(track, init)
  109 + }
  110 +
  111 + override fun addTransceiver(mediaType: MediaStreamTrack.MediaType?): RtpTransceiver {
  112 + return super.addTransceiver(mediaType)
  113 + }
  114 +
  115 + override fun addTransceiver(
  116 + mediaType: MediaStreamTrack.MediaType?,
  117 + init: RtpTransceiver.RtpTransceiverInit?
  118 + ): RtpTransceiver {
  119 + return super.addTransceiver(mediaType, init)
  120 + }
  121 +
  122 + override fun getStats(observer: StatsObserver?, track: MediaStreamTrack?): Boolean {
  123 + observer?.onComplete(emptyArray())
  124 + return true
  125 + }
  126 +
  127 + override fun getStats(callback: RTCStatsCollectorCallback?) {
  128 + callback?.onStatsDelivered(RTCStatsReport(0, emptyMap()))
  129 + }
  130 +
  131 + override fun setBitrate(min: Int?, current: Int?, max: Int?): Boolean {
  132 + return true
  133 + }
  134 +
  135 + override fun startRtcEventLog(file_descriptor: Int, max_size_bytes: Int): Boolean {
  136 + return true
  137 + }
  138 +
  139 + override fun stopRtcEventLog() {
  140 + }
  141 +
  142 + override fun signalingState(): SignalingState {
  143 + return super.signalingState()
  144 + }
  145 +
  146 + override fun iceConnectionState(): IceConnectionState {
  147 + return super.iceConnectionState()
  148 + }
  149 +
  150 + override fun connectionState(): PeerConnectionState {
  151 + return super.connectionState()
  152 + }
  153 +
  154 + override fun iceGatheringState(): IceGatheringState {
  155 + return super.iceGatheringState()
  156 + }
  157 +
  158 + override fun close() {
  159 + }
  160 +
  161 + override fun dispose() {
  162 + }
  163 +
  164 + override fun getNativePeerConnection(): Long = 0L
  165 +}
  1 +package io.livekit.android.mock
  2 +
  3 +import io.livekit.android.room.PeerConnectionTransport
  4 +import kotlinx.coroutines.CoroutineDispatcher
  5 +import org.webrtc.PeerConnection
  6 +import org.webrtc.PeerConnectionFactory
  7 +
  8 +internal class MockPeerConnectionTransportFactory(
  9 + private val dispatcher: CoroutineDispatcher,
  10 +) : PeerConnectionTransport.Factory {
  11 + override fun create(
  12 + config: PeerConnection.RTCConfiguration,
  13 + pcObserver: PeerConnection.Observer,
  14 + listener: PeerConnectionTransport.Listener?
  15 + ): PeerConnectionTransport {
  16 + return PeerConnectionTransport(
  17 + config,
  18 + pcObserver,
  19 + listener,
  20 + dispatcher,
  21 + PeerConnectionFactory.builder()
  22 + .createPeerConnectionFactory()
  23 + )
  24 + }
  25 +}
  1 +package io.livekit.android.mock
  2 +
  3 +import okhttp3.Request
  4 +import okhttp3.WebSocket
  5 +import okhttp3.WebSocketListener
  6 +import org.mockito.Mockito
  7 +
  8 +class MockWebsocketFactory : WebSocket.Factory {
  9 + lateinit var ws: WebSocket
  10 + lateinit var request: Request
  11 + lateinit var listener: WebSocketListener
  12 + override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
  13 + this.ws = Mockito.mock(WebSocket::class.java)
  14 + this.listener = listener
  15 + this.request = request
  16 + return ws
  17 + }
  18 +}
  1 +package io.livekit.android.mock.dagger
  2 +
  3 +import dagger.Module
  4 +import dagger.Provides
  5 +import io.livekit.android.dagger.InjectionNames
  6 +import kotlinx.coroutines.CoroutineDispatcher
  7 +import kotlinx.coroutines.ExperimentalCoroutinesApi
  8 +import kotlinx.coroutines.test.TestCoroutineDispatcher
  9 +import javax.inject.Named
  10 +
  11 +@Module
  12 +object TestCoroutinesModule {
  13 +
  14 + @OptIn(ExperimentalCoroutinesApi::class)
  15 + val coroutineDispatcher: CoroutineDispatcher = TestCoroutineDispatcher()
  16 +
  17 + @Provides
  18 + @Named(InjectionNames.DISPATCHER_DEFAULT)
  19 + fun defaultDispatcher() = coroutineDispatcher
  20 +
  21 + @Provides
  22 + @Named(InjectionNames.DISPATCHER_IO)
  23 + fun ioDispatcher() = coroutineDispatcher
  24 +
  25 + @Provides
  26 + @Named(InjectionNames.DISPATCHER_MAIN)
  27 + fun mainDispatcher() = coroutineDispatcher
  28 +
  29 + @Provides
  30 + @Named(InjectionNames.DISPATCHER_UNCONFINED)
  31 + fun unconfinedDispatcher() = coroutineDispatcher
  32 +}
  1 +package io.livekit.android.mock.dagger
  2 +
  3 +import android.content.Context
  4 +import dagger.BindsInstance
  5 +import dagger.Component
  6 +import io.livekit.android.dagger.JsonFormatModule
  7 +import io.livekit.android.dagger.LiveKitComponent
  8 +import io.livekit.android.mock.MockWebsocketFactory
  9 +import javax.inject.Singleton
  10 +
  11 +@Singleton
  12 +@Component(
  13 + modules = [
  14 + TestCoroutinesModule::class,
  15 + TestRTCModule::class,
  16 + TestWebModule::class,
  17 + JsonFormatModule::class,
  18 + ]
  19 +)
  20 +interface TestLiveKitComponent : LiveKitComponent {
  21 +
  22 + fun websocketFactory(): MockWebsocketFactory
  23 +
  24 + @Component.Factory
  25 + interface Factory {
  26 + fun create(@BindsInstance appContext: Context): TestLiveKitComponent
  27 + }
  28 +}
  1 +package io.livekit.android.mock.dagger
  2 +
  3 +import android.content.Context
  4 +import dagger.Module
  5 +import dagger.Provides
  6 +import io.livekit.android.dagger.InjectionNames
  7 +import io.livekit.android.mock.MockEglBase
  8 +import org.webrtc.*
  9 +import javax.inject.Named
  10 +import javax.inject.Singleton
  11 +
  12 +
  13 +@Module
  14 +object TestRTCModule {
  15 +
  16 + @Provides
  17 + @Singleton
  18 + fun eglBase(): EglBase {
  19 + return MockEglBase()
  20 + }
  21 +
  22 + @Provides
  23 + fun eglContext(eglBase: EglBase): EglBase.Context = eglBase.eglBaseContext
  24 +
  25 +
  26 + @Provides
  27 + @Singleton
  28 + fun peerConnectionFactory(
  29 + appContext: Context
  30 + ): PeerConnectionFactory {
  31 + try {
  32 + ContextUtils.initialize(appContext)
  33 + NativeLibraryLoaderTestHelper.initialize()
  34 + } catch (e: Throwable) {
  35 + // do nothing. this is expected.
  36 + }
  37 +
  38 + return MockPeerConnectionFactory()
  39 + }
  40 +
  41 + @Provides
  42 + @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)
  43 + fun videoHwAccel() = true
  44 +}
  1 +package io.livekit.android.mock.dagger
  2 +
  3 +import dagger.Module
  4 +import dagger.Provides
  5 +import io.livekit.android.mock.MockWebsocketFactory
  6 +import okhttp3.OkHttpClient
  7 +import okhttp3.Response
  8 +import okhttp3.WebSocket
  9 +import javax.inject.Singleton
  10 +
  11 +@Module
  12 +object TestWebModule {
  13 +
  14 + @Provides
  15 + @Singleton
  16 + fun okHttpClient(): OkHttpClient {
  17 + return OkHttpClient.Builder()
  18 + .addInterceptor {
  19 + // Don't make actual network calls
  20 + Response.Builder()
  21 + .code(200)
  22 + .build()
  23 + }
  24 + .build()
  25 + }
  26 +
  27 + @Provides
  28 + @Singleton
  29 + fun websocketFactory(websocketFactory: MockWebsocketFactory): WebSocket.Factory {
  30 + return websocketFactory
  31 + }
  32 +
  33 + @Provides
  34 + @Singleton
  35 + fun mockWebsocketFactory(): MockWebsocketFactory {
  36 + return MockWebsocketFactory()
  37 + }
  38 +}
  1 +package io.livekit.android.room
  2 +
  3 +import android.content.Context
  4 +import androidx.test.core.app.ApplicationProvider
  5 +import io.livekit.android.mock.MockWebsocketFactory
  6 +import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent
  7 +import io.livekit.android.util.toOkioByteString
  8 +import kotlinx.coroutines.CoroutineExceptionHandler
  9 +import kotlinx.coroutines.ExperimentalCoroutinesApi
  10 +import kotlinx.coroutines.launch
  11 +import kotlinx.coroutines.test.TestCoroutineScope
  12 +import kotlinx.coroutines.test.runBlockingTest
  13 +import org.junit.Assert
  14 +import org.junit.Before
  15 +import org.junit.Rule
  16 +import org.junit.Test
  17 +import org.junit.runner.RunWith
  18 +import org.mockito.junit.MockitoJUnit
  19 +import org.robolectric.RobolectricTestRunner
  20 +
  21 +@ExperimentalCoroutinesApi
  22 +@RunWith(RobolectricTestRunner::class)
  23 +class RoomMockE2ETest {
  24 +
  25 + @get:Rule
  26 + var mockitoRule = MockitoJUnit.rule()
  27 +
  28 + lateinit var context: Context
  29 + lateinit var room: Room
  30 + lateinit var wsFactory: MockWebsocketFactory
  31 +
  32 + @Before
  33 + fun setup() {
  34 + context = ApplicationProvider.getApplicationContext()
  35 + val component = DaggerTestLiveKitComponent
  36 + .factory()
  37 + .create(context)
  38 +
  39 + room = component.roomFactory()
  40 + .create(context)
  41 + wsFactory = component.websocketFactory()
  42 + }
  43 +
  44 + @Test
  45 + fun connectTest() {
  46 + val job = TestCoroutineScope().launch {
  47 + room.connect(
  48 + url = "http://www.example.com",
  49 + token = "",
  50 + options = null
  51 + )
  52 + }
  53 +
  54 + wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.JOIN.toOkioByteString())
  55 +
  56 + runBlockingTest {
  57 + job.join()
  58 + }
  59 + }
  60 +
  61 + @Test
  62 + fun roomUpdateTest() {
  63 + val handler = CoroutineExceptionHandler { _, exception ->
  64 + println("CoroutineExceptionHandler got $exception")
  65 + exception.printStackTrace()
  66 + }
  67 + val job = TestCoroutineScope().launch(handler) {
  68 + room.connect(
  69 + url = "http://www.example.com",
  70 + token = "",
  71 + options = null
  72 + )
  73 + }
  74 +
  75 + wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.JOIN.toOkioByteString())
  76 +
  77 + runBlockingTest {
  78 + job.join()
  79 + }
  80 + wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.ROOM_UPDATE.toOkioByteString())
  81 +
  82 + Assert.assertEquals(
  83 + SignalClientTest.ROOM_UPDATE.roomUpdate.room.metadata,
  84 + room.metadata
  85 + )
  86 + }
  87 +}
@@ -2,7 +2,7 @@ package io.livekit.android.room @@ -2,7 +2,7 @@ package io.livekit.android.room
2 2
3 import android.content.Context 3 import android.content.Context
4 import androidx.test.core.app.ApplicationProvider 4 import androidx.test.core.app.ApplicationProvider
5 -import io.livekit.android.room.mock.MockEglBase 5 +import io.livekit.android.mock.MockEglBase
6 import io.livekit.android.room.participant.LocalParticipant 6 import io.livekit.android.room.participant.LocalParticipant
7 import kotlinx.coroutines.ExperimentalCoroutinesApi 7 import kotlinx.coroutines.ExperimentalCoroutinesApi
8 import kotlinx.coroutines.launch 8 import kotlinx.coroutines.launch
@@ -18,7 +18,6 @@ import org.mockito.Mockito @@ -18,7 +18,6 @@ import org.mockito.Mockito
18 import org.mockito.junit.MockitoJUnit 18 import org.mockito.junit.MockitoJUnit
19 import org.robolectric.RobolectricTestRunner 19 import org.robolectric.RobolectricTestRunner
20 import org.webrtc.EglBase 20 import org.webrtc.EglBase
21 -import org.webrtc.PeerConnectionFactory  
22 21
23 @ExperimentalCoroutinesApi 22 @ExperimentalCoroutinesApi
24 @RunWith(RobolectricTestRunner::class) 23 @RunWith(RobolectricTestRunner::class)
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
3 import com.google.protobuf.util.JsonFormat 3 import com.google.protobuf.util.JsonFormat
  4 +import io.livekit.android.mock.MockWebsocketFactory
4 import io.livekit.android.util.toOkioByteString 5 import io.livekit.android.util.toOkioByteString
5 import kotlinx.coroutines.ExperimentalCoroutinesApi 6 import kotlinx.coroutines.ExperimentalCoroutinesApi
6 import kotlinx.coroutines.async 7 import kotlinx.coroutines.async
@@ -29,16 +30,6 @@ class SignalClientTest { @@ -29,16 +30,6 @@ class SignalClientTest {
29 lateinit var coroutineDispatcher: TestCoroutineDispatcher 30 lateinit var coroutineDispatcher: TestCoroutineDispatcher
30 lateinit var coroutineScope: TestCoroutineScope 31 lateinit var coroutineScope: TestCoroutineScope
31 32
32 - class MockWebsocketFactory : WebSocket.Factory {  
33 - lateinit var ws: WebSocket  
34 - lateinit var request: Request  
35 - override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {  
36 - ws = Mockito.mock(WebSocket::class.java)  
37 - this.request = request  
38 - return ws  
39 - }  
40 - }  
41 -  
42 @Before 33 @Before
43 fun setup() { 34 fun setup() {
44 coroutineDispatcher = TestCoroutineDispatcher() 35 coroutineDispatcher = TestCoroutineDispatcher()
@@ -139,7 +130,7 @@ class SignalClientTest { @@ -139,7 +130,7 @@ class SignalClientTest {
139 companion object { 130 companion object {
140 private const val EXAMPLE_URL = "http://www.example.com" 131 private const val EXAMPLE_URL = "http://www.example.com"
141 132
142 - private val JOIN = with(LivekitRtc.SignalResponse.newBuilder()) { 133 + val JOIN = with(LivekitRtc.SignalResponse.newBuilder()) {
143 join = with(joinBuilder) { 134 join = with(joinBuilder) {
144 room = with(roomBuilder) { 135 room = with(roomBuilder) {
145 name = "roomname" 136 name = "roomname"
@@ -150,7 +141,8 @@ class SignalClientTest { @@ -150,7 +141,8 @@ class SignalClientTest {
150 } 141 }
151 build() 142 build()
152 } 143 }
153 - private val OFFER = with(LivekitRtc.SignalResponse.newBuilder()) { 144 +
  145 + val OFFER = with(LivekitRtc.SignalResponse.newBuilder()) {
154 offer = with(offerBuilder) { 146 offer = with(offerBuilder) {
155 sdp = "" 147 sdp = ""
156 type = "offer" 148 type = "offer"
@@ -158,5 +150,16 @@ class SignalClientTest { @@ -158,5 +150,16 @@ class SignalClientTest {
158 } 150 }
159 build() 151 build()
160 } 152 }
  153 +
  154 + val ROOM_UPDATE = with(LivekitRtc.SignalResponse.newBuilder()) {
  155 + roomUpdate = with(roomUpdateBuilder) {
  156 + room = with(roomBuilder) {
  157 + metadata = "metadata"
  158 + build()
  159 + }
  160 + build()
  161 + }
  162 + build()
  163 + }
161 } 164 }
162 } 165 }
  1 +package org.webrtc
  2 +
  3 +import io.livekit.android.mock.MockPeerConnection
  4 +
  5 +class MockPeerConnectionFactory : PeerConnectionFactory(1L) {
  6 + override fun createPeerConnectionInternal(
  7 + rtcConfig: PeerConnection.RTCConfiguration?,
  8 + constraints: MediaConstraints?,
  9 + observer: PeerConnection.Observer?,
  10 + sslCertificateVerifier: SSLCertificateVerifier?
  11 + ): PeerConnection {
  12 + return MockPeerConnection(observer)
  13 + }
  14 +}
  1 +package org.webrtc
  2 +
  3 +object NativeLibraryLoaderTestHelper {
  4 + fun initialize() {
  5 + NativeLibrary.initialize({ true }, "")
  6 + }
  7 +}