David Liu

tests for events

@@ -35,13 +35,19 @@ sealed class RoomEvent : Event() { @@ -35,13 +35,19 @@ sealed class RoomEvent : Event() {
35 /** 35 /**
36 * When a [RemoteParticipant] leaves after the local participant has joined. 36 * When a [RemoteParticipant] leaves after the local participant has joined.
37 */ 37 */
38 - class ParticipantDisconnected(val room: Room, val participant: RemoteParticipant): RoomEvent() 38 + class ParticipantDisconnected(val room: Room, val participant: RemoteParticipant) : RoomEvent()
39 39
40 /** 40 /**
41 * Active speakers changed. List of speakers are ordered by their audio level. loudest 41 * Active speakers changed. List of speakers are ordered by their audio level. loudest
42 * speakers first. This will include the [LocalParticipant] too. 42 * speakers first. This will include the [LocalParticipant] too.
43 */ 43 */
44 - class ActiveSpeakersChanged(val room: Room, val speakers: List<Participant>,): RoomEvent() 44 + class ActiveSpeakersChanged(val room: Room, val speakers: List<Participant>) : RoomEvent()
  45 +
  46 + class RoomMetadataChanged(
  47 + val room: Room,
  48 + val newMetadata: String?,
  49 + val prevMetadata: String?
  50 + ) : RoomEvent()
45 51
46 // Participant callbacks 52 // Participant callbacks
47 /** 53 /**
@@ -49,7 +55,11 @@ sealed class RoomEvent : Event() { @@ -49,7 +55,11 @@ sealed class RoomEvent : Event() {
49 * When RoomService.UpdateParticipantMetadata is called to change a participant's state, 55 * When RoomService.UpdateParticipantMetadata is called to change a participant's state,
50 * this event will be fired for all clients in the room. 56 * this event will be fired for all clients in the room.
51 */ 57 */
52 - class MetadataChanged(val room: Room, val participant: Participant, val prevMetadata: String?): RoomEvent() 58 + class ParticipantMetadataChanged(
  59 + val room: Room,
  60 + val participant: Participant,
  61 + val prevMetadata: String?
  62 + ) : RoomEvent()
53 63
54 /** 64 /**
55 * The participant was muted. 65 * The participant was muted.
@@ -349,7 +349,10 @@ constructor( @@ -349,7 +349,10 @@ constructor(
349 } 349 }
350 350
351 override fun onRoomUpdate(update: LivekitModels.Room) { 351 override fun onRoomUpdate(update: LivekitModels.Room) {
  352 + val oldMetadata = metadata
352 metadata = update.metadata 353 metadata = update.metadata
  354 +
  355 + eventBus.postEvent(RoomEvent.RoomMetadataChanged(this, metadata, oldMetadata), coroutineScope)
353 } 356 }
354 357
355 override fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) { 358 override fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) {
@@ -403,7 +406,7 @@ constructor( @@ -403,7 +406,7 @@ constructor(
403 */ 406 */
404 override fun onMetadataChanged(participant: Participant, prevMetadata: String?) { 407 override fun onMetadataChanged(participant: Participant, prevMetadata: String?) {
405 listener?.onMetadataChanged(participant, prevMetadata, this) 408 listener?.onMetadataChanged(participant, prevMetadata, this)
406 - eventBus.postEvent(RoomEvent.MetadataChanged(this, participant, prevMetadata), coroutineScope) 409 + eventBus.postEvent(RoomEvent.ParticipantMetadataChanged(this, participant, prevMetadata), coroutineScope)
407 } 410 }
408 411
409 /** @suppress */ 412 /** @suppress */
  1 +package io.livekit.android.coroutines
  2 +
  3 +import io.livekit.android.events.EventListenable
  4 +import kotlinx.coroutines.CancellationException
  5 +import kotlinx.coroutines.cancel
  6 +import kotlinx.coroutines.coroutineScope
  7 +import kotlinx.coroutines.flow.*
  8 +import kotlinx.coroutines.launch
  9 +
  10 +/**
  11 + * Collect events until signal is given.
  12 + */
  13 +suspend fun <T> EventListenable<T>.collectEvents(signal: Flow<Unit?>): List<T> {
  14 + return events.takeUntilSignal(signal)
  15 + .fold(emptyList()) { list, event ->
  16 + list.plus(event)
  17 + }
  18 +}
  19 +
  20 +fun <T> Flow<T>.takeUntilSignal(signal: Flow<Unit?>): Flow<T> = flow {
  21 + try {
  22 + coroutineScope {
  23 + launch {
  24 + signal.takeWhile { it == null }.collect()
  25 + println("signalled")
  26 + this@coroutineScope.cancel()
  27 + }
  28 +
  29 + collect {
  30 + emit(it)
  31 + }
  32 + }
  33 +
  34 + } catch (e: CancellationException) {
  35 + //ignore
  36 + }
  37 +}
  1 +package io.livekit.android.events
  2 +
  3 +import io.livekit.android.coroutines.collectEvents
  4 +import kotlinx.coroutines.CoroutineScope
  5 +import kotlinx.coroutines.ExperimentalCoroutinesApi
  6 +import kotlinx.coroutines.async
  7 +import kotlinx.coroutines.flow.MutableStateFlow
  8 +import kotlinx.coroutines.test.runBlockingTest
  9 +
  10 +class EventCollector<T : Event>(
  11 + private val eventListenable: EventListenable<T>,
  12 + coroutineScope: CoroutineScope
  13 +) {
  14 + val signal = MutableStateFlow<Unit?>(null)
  15 + val collectEventsDeferred = coroutineScope.async {
  16 + eventListenable.collectEvents(signal)
  17 + }
  18 +
  19 + /**
  20 + * Stop collecting events. returns the events collected.
  21 + */
  22 + @OptIn(ExperimentalCoroutinesApi::class)
  23 + fun stopCollectingEvents(): List<T> {
  24 + signal.compareAndSet(null, Unit)
  25 + var events: List<T> = emptyList()
  26 + runBlockingTest {
  27 + events = collectEventsDeferred.await()
  28 + }
  29 + return events
  30 + }
  31 +
  32 +}
  1 +package io.livekit.android.mock
  2 +
  3 +import livekit.LivekitModels
  4 +
  5 +object TestData {
  6 +
  7 + val REMOTE_AUDIO_TRACK = with(LivekitModels.TrackInfo.newBuilder()) {
  8 + sid = "remote_audio_track_sid"
  9 + type = LivekitModels.TrackType.AUDIO
  10 + build()
  11 + }
  12 +
  13 + val LOCAL_PARTICIPANT = with(LivekitModels.ParticipantInfo.newBuilder()) {
  14 + sid = "local_participant_sid"
  15 + identity = "local_participant_identity"
  16 + state = LivekitModels.ParticipantInfo.State.ACTIVE
  17 + build()
  18 + }
  19 +
  20 + val REMOTE_PARTICIPANT = with(LivekitModels.ParticipantInfo.newBuilder()) {
  21 + sid = "remote_participant_sid"
  22 + identity = "remote_participant_identity"
  23 + state = LivekitModels.ParticipantInfo.State.ACTIVE
  24 + addTracks(REMOTE_AUDIO_TRACK)
  25 + build()
  26 + }
  27 +
  28 +
  29 + val REMOTE_SPEAKER_INFO = with(LivekitModels.SpeakerInfo.newBuilder()) {
  30 + sid = REMOTE_PARTICIPANT.sid
  31 + level = 1.0f
  32 + active = true
  33 + build()
  34 + }
  35 +}
@@ -3,21 +3,20 @@ package io.livekit.android.room @@ -3,21 +3,20 @@ package io.livekit.android.room
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.coroutines.TestCoroutineRule 5 import io.livekit.android.coroutines.TestCoroutineRule
  6 +import io.livekit.android.events.EventCollector
  7 +import io.livekit.android.events.RoomEvent
6 import io.livekit.android.mock.MockWebsocketFactory 8 import io.livekit.android.mock.MockWebsocketFactory
7 import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent 9 import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent
8 import io.livekit.android.room.participant.ConnectionQuality 10 import io.livekit.android.room.participant.ConnectionQuality
9 import io.livekit.android.util.toOkioByteString 11 import io.livekit.android.util.toOkioByteString
10 import kotlinx.coroutines.ExperimentalCoroutinesApi 12 import kotlinx.coroutines.ExperimentalCoroutinesApi
11 -import kotlinx.coroutines.Job  
12 import kotlinx.coroutines.launch 13 import kotlinx.coroutines.launch
13 -import kotlinx.coroutines.test.TestCoroutineScope  
14 import kotlinx.coroutines.test.runBlockingTest 14 import kotlinx.coroutines.test.runBlockingTest
15 import org.junit.Assert 15 import org.junit.Assert
16 import org.junit.Before 16 import org.junit.Before
17 import org.junit.Rule 17 import org.junit.Rule
18 import org.junit.Test 18 import org.junit.Test
19 import org.junit.runner.RunWith 19 import org.junit.runner.RunWith
20 -import org.mockito.Mockito  
21 import org.mockito.junit.MockitoJUnit 20 import org.mockito.junit.MockitoJUnit
22 import org.robolectric.RobolectricTestRunner 21 import org.robolectric.RobolectricTestRunner
23 22
@@ -71,30 +70,114 @@ class RoomMockE2ETest { @@ -71,30 +70,114 @@ class RoomMockE2ETest {
71 @Test 70 @Test
72 fun roomUpdateTest() { 71 fun roomUpdateTest() {
73 connect() 72 connect()
  73 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
74 wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.ROOM_UPDATE.toOkioByteString()) 74 wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.ROOM_UPDATE.toOkioByteString())
  75 + val events = eventCollector.stopCollectingEvents()
75 76
76 Assert.assertEquals( 77 Assert.assertEquals(
77 SignalClientTest.ROOM_UPDATE.roomUpdate.room.metadata, 78 SignalClientTest.ROOM_UPDATE.roomUpdate.room.metadata,
78 room.metadata 79 room.metadata
79 ) 80 )
  81 + Assert.assertEquals(1, events.size)
  82 + Assert.assertEquals(true, events[0] is RoomEvent.RoomMetadataChanged)
80 } 83 }
81 84
82 @Test 85 @Test
83 fun connectionQualityUpdateTest() { 86 fun connectionQualityUpdateTest() {
84 - val roomListener = Mockito.mock(RoomListener::class.java)  
85 - room.listener = roomListener  
86 -  
87 connect() 87 connect()
  88 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
88 wsFactory.listener.onMessage( 89 wsFactory.listener.onMessage(
89 wsFactory.ws, 90 wsFactory.ws,
90 SignalClientTest.CONNECTION_QUALITY.toOkioByteString() 91 SignalClientTest.CONNECTION_QUALITY.toOkioByteString()
91 ) 92 )
  93 + val events = eventCollector.stopCollectingEvents()
92 94
93 - Assert.assertEquals(  
94 - ConnectionQuality.EXCELLENT,  
95 - room.localParticipant.connectionQuality 95 + Assert.assertEquals(ConnectionQuality.EXCELLENT, room.localParticipant.connectionQuality)
  96 + Assert.assertEquals(1, events.size)
  97 + Assert.assertEquals(true, events[0] is RoomEvent.ConnectionQualityChanged)
  98 + }
  99 +
  100 + @Test
  101 + fun participantConnected() {
  102 + connect()
  103 +
  104 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  105 + wsFactory.listener.onMessage(
  106 + wsFactory.ws,
  107 + SignalClientTest.PARTICIPANT_JOIN.toOkioByteString()
96 ) 108 )
97 - Mockito.verify(roomListener)  
98 - .onConnectionQualityChanged(room.localParticipant, ConnectionQuality.EXCELLENT) 109 + val events = eventCollector.stopCollectingEvents()
  110 +
  111 + Assert.assertEquals(1, events.size)
  112 + Assert.assertEquals(true, events[0] is RoomEvent.ParticipantConnected)
99 } 113 }
  114 +
  115 + @Test
  116 + fun participantDisconnected() {
  117 + connect()
  118 + wsFactory.listener.onMessage(
  119 + wsFactory.ws,
  120 + SignalClientTest.PARTICIPANT_JOIN.toOkioByteString()
  121 + )
  122 +
  123 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  124 + wsFactory.listener.onMessage(
  125 + wsFactory.ws,
  126 + SignalClientTest.PARTICIPANT_DISCONNECT.toOkioByteString()
  127 + )
  128 + val events = eventCollector.stopCollectingEvents()
  129 +
  130 + Assert.assertEquals(1, events.size)
  131 + Assert.assertEquals(true, events[0] is RoomEvent.ParticipantDisconnected)
  132 + }
  133 +
  134 + @Test
  135 + fun onActiveSpeakersChanged() {
  136 + connect()
  137 +
  138 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  139 + wsFactory.listener.onMessage(
  140 + wsFactory.ws,
  141 + SignalClientTest.ACTIVE_SPEAKER_UPDATE.toOkioByteString()
  142 + )
  143 + val events = eventCollector.stopCollectingEvents()
  144 +
  145 + Assert.assertEquals(1, events.size)
  146 + Assert.assertEquals(true, events[0] is RoomEvent.ActiveSpeakersChanged)
  147 + }
  148 +
  149 + @Test
  150 + fun participantMetadataChanged() {
  151 + connect()
  152 +
  153 + wsFactory.listener.onMessage(
  154 + wsFactory.ws,
  155 + SignalClientTest.PARTICIPANT_JOIN.toOkioByteString()
  156 + )
  157 +
  158 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  159 + wsFactory.listener.onMessage(
  160 + wsFactory.ws,
  161 + SignalClientTest.PARTICIPANT_METADATA_CHANGED.toOkioByteString()
  162 + )
  163 + val events = eventCollector.stopCollectingEvents()
  164 +
  165 + Assert.assertEquals(1, events.size)
  166 + Assert.assertEquals(true, events[0] is RoomEvent.ParticipantMetadataChanged)
  167 + }
  168 +
  169 + @Test
  170 + fun leave() {
  171 + connect()
  172 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  173 + wsFactory.listener.onMessage(
  174 + wsFactory.ws,
  175 + SignalClientTest.LEAVE.toOkioByteString()
  176 + )
  177 + val events = eventCollector.stopCollectingEvents()
  178 +
  179 + Assert.assertEquals(1, events.size)
  180 + Assert.assertEquals(true, events[0] is RoomEvent.Disconnected)
  181 + }
  182 +
100 } 183 }
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
3 import android.content.Context 3 import android.content.Context
  4 +import android.net.Network
4 import androidx.test.core.app.ApplicationProvider 5 import androidx.test.core.app.ApplicationProvider
5 import io.livekit.android.coroutines.TestCoroutineRule 6 import io.livekit.android.coroutines.TestCoroutineRule
  7 +import io.livekit.android.coroutines.collectEvents
  8 +import io.livekit.android.events.Event
  9 +import io.livekit.android.events.EventCollector
  10 +import io.livekit.android.events.EventListenable
  11 +import io.livekit.android.events.RoomEvent
6 import io.livekit.android.mock.MockEglBase 12 import io.livekit.android.mock.MockEglBase
  13 +import io.livekit.android.mock.TestData
7 import io.livekit.android.room.participant.LocalParticipant 14 import io.livekit.android.room.participant.LocalParticipant
8 -import kotlinx.coroutines.ExperimentalCoroutinesApi  
9 -import kotlinx.coroutines.launch 15 +import kotlinx.coroutines.*
  16 +import kotlinx.coroutines.flow.MutableStateFlow
10 import kotlinx.coroutines.test.runBlockingTest 17 import kotlinx.coroutines.test.runBlockingTest
11 import livekit.LivekitModels 18 import livekit.LivekitModels
  19 +import org.junit.Assert
12 import org.junit.Before 20 import org.junit.Before
13 import org.junit.Rule 21 import org.junit.Rule
14 import org.junit.Test 22 import org.junit.Test
@@ -56,16 +64,20 @@ class RoomTest { @@ -56,16 +64,20 @@ class RoomTest {
56 eglBase, 64 eglBase,
57 localParticantFactory, 65 localParticantFactory,
58 DefaultsManager(), 66 DefaultsManager(),
59 - coroutineRule.dispatcher 67 + coroutineRule.dispatcher,
  68 + coroutineRule.dispatcher,
60 ) 69 )
61 } 70 }
62 71
63 - @Test  
64 - fun connectTest() { 72 + fun connect() {
65 rtcEngine.stub { 73 rtcEngine.stub {
66 onBlocking { rtcEngine.join(any(), any(), anyOrNull()) } 74 onBlocking { rtcEngine.join(any(), any(), anyOrNull()) }
67 .doReturn(SignalClientTest.JOIN.join) 75 .doReturn(SignalClientTest.JOIN.join)
68 } 76 }
  77 + rtcEngine.stub {
  78 + onBlocking { rtcEngine.client }
  79 + .doReturn(Mockito.mock(SignalClient::class.java))
  80 + }
69 val job = coroutineRule.scope.launch { 81 val job = coroutineRule.scope.launch {
70 room.connect( 82 room.connect(
71 url = "http://www.example.com", 83 url = "http://www.example.com",
@@ -77,4 +89,36 @@ class RoomTest { @@ -77,4 +89,36 @@ class RoomTest {
77 job.join() 89 job.join()
78 } 90 }
79 } 91 }
  92 +
  93 + @Test
  94 + fun connectTest() {
  95 + connect()
  96 + }
  97 +
  98 + @Test
  99 + fun onConnectionAvailableWillReconnect() {
  100 + connect()
  101 +
  102 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  103 + val network = Mockito.mock(Network::class.java)
  104 + room.onLost(network)
  105 + room.onAvailable(network)
  106 +
  107 + val events = eventCollector.stopCollectingEvents()
  108 +
  109 + Assert.assertEquals(1, events.size)
  110 + Assert.assertEquals(true, events[0] is RoomEvent.Reconnecting)
  111 + }
  112 +
  113 + @Test
  114 + fun onDisconnect() {
  115 + connect()
  116 +
  117 + val eventCollector = EventCollector(room.events, coroutineRule.scope)
  118 + room.onDisconnect("")
  119 + val events = eventCollector.stopCollectingEvents()
  120 +
  121 + Assert.assertEquals(1, events.size)
  122 + Assert.assertEquals(true, events[0] is RoomEvent.Disconnected)
  123 + }
80 } 124 }
@@ -2,6 +2,7 @@ package io.livekit.android.room @@ -2,6 +2,7 @@ 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.mock.MockWebsocketFactory
  5 +import io.livekit.android.mock.TestData
5 import io.livekit.android.util.toOkioByteString 6 import io.livekit.android.util.toOkioByteString
6 import kotlinx.coroutines.ExperimentalCoroutinesApi 7 import kotlinx.coroutines.ExperimentalCoroutinesApi
7 import kotlinx.coroutines.async 8 import kotlinx.coroutines.async
@@ -11,7 +12,10 @@ import kotlinx.coroutines.test.runBlockingTest @@ -11,7 +12,10 @@ import kotlinx.coroutines.test.runBlockingTest
11 import kotlinx.serialization.json.Json 12 import kotlinx.serialization.json.Json
12 import livekit.LivekitModels 13 import livekit.LivekitModels
13 import livekit.LivekitRtc 14 import livekit.LivekitRtc
14 -import okhttp3.* 15 +import okhttp3.OkHttpClient
  16 +import okhttp3.Protocol
  17 +import okhttp3.Request
  18 +import okhttp3.Response
15 import org.junit.After 19 import org.junit.After
16 import org.junit.Assert 20 import org.junit.Assert
17 import org.junit.Before 21 import org.junit.Before
@@ -138,11 +142,7 @@ class SignalClientTest { @@ -138,11 +142,7 @@ class SignalClientTest {
138 sid = "room_sid" 142 sid = "room_sid"
139 build() 143 build()
140 } 144 }
141 - participant = with(participantBuilder) {  
142 - sid = "participant_sid"  
143 - identity = "participant_identity"  
144 - build()  
145 - } 145 + participant = TestData.LOCAL_PARTICIPANT
146 build() 146 build()
147 } 147 }
148 build() 148 build()
@@ -168,9 +168,58 @@ class SignalClientTest { @@ -168,9 +168,58 @@ class SignalClientTest {
168 build() 168 build()
169 } 169 }
170 170
  171 + val TRACK_PUBLISHED = with(LivekitRtc.SignalResponse.newBuilder()) {
  172 + trackPublished = with(trackPublishedBuilder) {
  173 + track = TestData.REMOTE_AUDIO_TRACK
  174 + build()
  175 + }
  176 + build()
  177 + }
  178 +
  179 + val PARTICIPANT_JOIN = with(LivekitRtc.SignalResponse.newBuilder()) {
  180 + update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
  181 + addParticipants(TestData.REMOTE_PARTICIPANT)
  182 + build()
  183 + }
  184 + build()
  185 + }
  186 +
  187 + val PARTICIPANT_DISCONNECT = with(LivekitRtc.SignalResponse.newBuilder()) {
  188 + update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
  189 + val disconnectedParticipant = TestData.REMOTE_PARTICIPANT.toBuilder()
  190 + .setState(LivekitModels.ParticipantInfo.State.DISCONNECTED)
  191 + .build()
  192 +
  193 + addParticipants(disconnectedParticipant)
  194 + build()
  195 + }
  196 + build()
  197 + }
  198 +
  199 + val ACTIVE_SPEAKER_UPDATE = with(LivekitRtc.SignalResponse.newBuilder()) {
  200 + speakersChanged = with(LivekitRtc.SpeakersChanged.newBuilder()) {
  201 + addSpeakers(TestData.REMOTE_SPEAKER_INFO)
  202 + build()
  203 + }
  204 + build()
  205 + }
  206 +
  207 + val PARTICIPANT_METADATA_CHANGED = with(LivekitRtc.SignalResponse.newBuilder()) {
  208 + update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
  209 + val participantMetadataChanged = TestData.REMOTE_PARTICIPANT.toBuilder()
  210 + .setMetadata("changed_metadata")
  211 + .build()
  212 +
  213 + addParticipants(participantMetadataChanged)
  214 + build()
  215 + }
  216 + build()
  217 + }
  218 +
  219 +
171 val CONNECTION_QUALITY = with(LivekitRtc.SignalResponse.newBuilder()) { 220 val CONNECTION_QUALITY = with(LivekitRtc.SignalResponse.newBuilder()) {
172 connectionQuality = with(connectionQualityBuilder) { 221 connectionQuality = with(connectionQualityBuilder) {
173 - addUpdates(with(LivekitRtc.ConnectionQualityInfo.newBuilder()){ 222 + addUpdates(with(LivekitRtc.ConnectionQualityInfo.newBuilder()) {
174 participantSid = JOIN.join.participant.sid 223 participantSid = JOIN.join.participant.sid
175 quality = LivekitModels.ConnectionQuality.EXCELLENT 224 quality = LivekitModels.ConnectionQuality.EXCELLENT
176 build() 225 build()
@@ -179,5 +228,12 @@ class SignalClientTest { @@ -179,5 +228,12 @@ class SignalClientTest {
179 } 228 }
180 build() 229 build()
181 } 230 }
  231 +
  232 + val LEAVE = with(LivekitRtc.SignalResponse.newBuilder()) {
  233 + leave = with(leaveBuilder) {
  234 + build()
  235 + }
  236 + build()
  237 + }
182 } 238 }
183 } 239 }