reactormonk
07/24/2023, 9:45 AMsuspend fun reloadData() {
try {
Log.d(TAG, "Sup4")
delay(500)
Log.d(TAG, "Sup4+")
val feedWithVideos = dao.getRssFeedWithUnorderedVideos(url)
Log.d(TAG, "Sup4++, feedWithVideos: $feedWithVideos")
data = feedWithVideos.let { feed ->
feed.copy(
feedPosition = feed.feedPosition,
videos = feed.videos.sortedByDescending { it.pubDate }
)
}
Log.d(TAG, "Sup5")
Log.d(TAG, "Reloaded, data: $data")
} catch (e: Exception) {
Log.e(TAG, "Exception in reloadData: ", e)
}
}
Which I'm calling via
LaunchedEffect(false) {
Log.d(TAG, "Sup3")
reloadData()
val feedData = getNewestFeed(url).sortedByDescending { it.pubDate }
if (data.videos.firstOrNull() != feedData.firstOrNull()) {
dao.insertAll(*feedData.toTypedArray())
dao.deletePosition(url)
reloadData()
}
}
Which then hangs on the first delay ("Sup4+" doesn't show up), because getNewestFeed
blocks (haven't found a coroutine-enabled HTTP client).Joffrey
07/24/2023, 9:47 AMhaven't found a coroutine-enabled HTTP clientKtor?
CLOVIS
07/24/2023, 9:48 AMWhich then hangs on the first delayI don't see anything that could block in your example before the
"Sup4+"
message, are you sure you showed the entire thing?Joffrey
07/24/2023, 9:48 AMbecause getNewestFeed blocksI'm not sure why this call would affect the display of
Sub4+
in your case. reloadData
is suspending and is called before getNewestFeed
, so getNewestFeed
shouldn't be called before reloadData
is done and returnsCLOVIS
07/24/2023, 9:49 AMSup4+
doesn't get printed, is if an exception is thrownJoffrey
07/24/2023, 9:50 AMtry-catch
with the very general Exception
is a bad idea. In general, but also especially in coroutines, because CancellationException
is an exception used by coroutines to propagate cancellation, and it shouldn't be caught. Even if that's not the cause of your problem here (please confirm the exception handling log is not printed), it should still be fixedreactormonk
07/24/2023, 9:51 AMCLOVIS
07/24/2023, 9:51 AMcatch (e: Exception)
is a bug, do not keep it in prodJoffrey
07/24/2023, 9:52 AMThe try/catch is only for debugging purposesEven in that case, I'd suggest adding
throw e
just after the log, so your logging is "transparent"reactormonk
07/24/2023, 9:52 AMAsyncException
/ Exception
like in Haskell?CLOVIS
07/24/2023, 9:53 AMreactormonk
07/24/2023, 9:53 AMJoffrey
07/24/2023, 9:54 AMThe http call failsWhich one? Because as we said
getNewestFeed
shouldn't even be called before Sup4+
is printed in your caseCLOVIS
07/24/2023, 9:54 AMreactormonk
07/24/2023, 9:55 AMgetNewestFeed
reactormonk
07/24/2023, 9:55 AMShadowLog.stream = System.out // Redirect Logcat to console
, maybe that affects things?Joffrey
07/24/2023, 9:56 AMreloadData
and getNewestFeed
to confirm that this doesn't happenJoffrey
07/24/2023, 9:57 AMmaybe that affects things?I can't answer that, I'm not familiar with this tooling
reactormonk
07/24/2023, 9:57 AMgetNewestFeed
and reran everythingreactormonk
07/24/2023, 9:57 AMD/RssNewsActivity: Sup1
I/ExoPlayerImpl: Init 11873dbb [AndroidXMedia3/1.1.0] [robolectric, robolectric, robolectric, 33]
D/RssNewsActivity: Sup2
D/RssNewsActivity: Sup3
D/RssNewsActivity: Sup4
0
CLOVIS
07/24/2023, 9:58 AMreactormonk
07/24/2023, 9:59 AM@Composable
fun RssNewsCompose(dao: RssFeedDao, url: HttpUrl, exoPlayerBuilder: (Context) -> ExoPlayer) {
val TAG = "RssNewsActivity"
var data by remember { mutableStateOf(RssFeedWithVideos(null, listOf())) }
suspend fun reloadData() {
Log.d(TAG, "Sup4")
delay(500)
Log.d(TAG, "Sup4+")
val feedWithVideos = dao.getRssFeedWithUnorderedVideos(url)
Log.d(TAG, "Sup4++, feedWithVideos: $feedWithVideos")
data = feedWithVideos.let { feed ->
feed.copy(
feedPosition = feed.feedPosition,
videos = feed.videos.sortedByDescending { it.pubDate }
)
}
Log.d(TAG, "Sup5")
Log.d(TAG, "Reloaded, data: $data")
}
Log.d(TAG, "Sup1")
AutoPlayPlaylist(data = data, dao = dao, exoPlayerBuilder = exoPlayerBuilder)
Log.d(TAG, "Sup2")
LaunchedEffect(false) {
Log.d(TAG, "Sup3")
reloadData()
// val feedData = getNewestFeed(url).sortedByDescending { it.pubDate }
// if (data.videos.firstOrNull() != feedData.firstOrNull()) {
// dao.insertAll(*feedData.toTypedArray())
// dao.deletePosition(url)
// reloadData()
// }
}
}
The test:
@RunWith(RobolectricTestRunner::class)
//@Config(sdk = [33], shadows = [ ShadowMediaMetricsManager::class ])
@Config(sdk = [33])
class MediathekKtTest {
private lateinit var database: AppDatabase
val context: Context = ApplicationProvider.getApplicationContext<Context>()
@Before
fun setUp() {
System.setProperty("kotlinx.coroutines.debug", "")
ShadowLog.stream = System.out // Redirect Logcat to console
}
private fun refreshDB() {
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.build()
}
@get:Rule
val composeTestRule = createComposeRule()
@get:Rule
val globalTimeout: SharedTimeOut = SharedTimeOut.seconds(5)
val port = 10023
@OptIn(ExperimentalTime::class)
@Test
fun testBasicRss() {
refreshDB()
val server = static(ResourceLoader.Directory("media")).asServer(SunHttp(port)).start()
// ExoPlayer requires a media_metrics component, not provided by robolectric
var exo: ExoPlayer? = null
val capturing = CapturingRenderersFactory(context)
ShadowLog.stream = System.out // Redirect Logcat to console
val appShadow = shadowOf(RuntimeEnvironment.getApplication())
appShadow.setSystemService("media_metrics", mock<MediaMetricsManager> {
on { createPlaybackSession() } doReturn ( mock {} )
})
composeTestRule.setContent {
RssNewsCompose(database.rssFeedVideoDao(), "<http://localhost>:$port".toHttpUrl()) { context ->
ExoPlayer.Builder(context, capturing).build().also { exo = it }
}
}
runBlocking {
println(exo?.mediaItemCount)
while(exo?.isPlaying == false) {
println("Not Playing...")
delay(500)
}
while(exo?.isPlaying == true) {
println("Playing...")
delay(500)
}
}
composeTestRule.waitForIdle()
val dumper = Dumper()
capturing.dump(dumper)
println("***")
println(dumper.toString())
println("***")
val serializer = Persister()
val ijLog = serializer.read(IjLog::class.java, dumper.toString())
println(ijLog)
server.stop()
exo?.release()
}
}
reactormonk
07/24/2023, 9:59 AMtestBasicRss
test only.CLOVIS
07/24/2023, 10:03 AMCLOVIS
07/24/2023, 10:03 AMrunTest
instead of runBlocking
so delays are skippedJoffrey
07/24/2023, 10:05 AMyou should use runTest instead of runBlocking so delays are skippedDelay-skipping is sometimes undesirable if the SUT interacts with other timed systems that don't get the same virtual time injected into them (e.g. if they are separate processes, like a docker server, or if they simply have their own non-overridden production coroutine scope)
reactormonk
07/24/2023, 10:05 AMJoffrey
07/24/2023, 10:06 AMMaybe it gets cancelled because of your 5 seconds timeout?But wouldn't that still show the exception handling log? (Unless
Log.d
became suspending somewhere in the last 10 years, of course, which I wouldn't be aware of)reactormonk
07/24/2023, 10:06 AMD/RssNewsActivity: Sup1
I/ExoPlayerImpl: Init 11873dbb [AndroidXMedia3/1.1.0] [robolectric, robolectric, robolectric, 33]
D/RssNewsActivity: Sup2
D/RssNewsActivity: Sup3
D/RssNewsActivity: Sup4
0
Not Playing...
Not Playing...
Not Playing...
Not Playing...
D/LifecycleMonitor: Lifecycle status change: androidx.activity.ComponentActivity@4e8c368c in: PAUSED
V/ActivityScenario: Update currentActivityStage to PAUSED, currentActivity=androidx.activity.ComponentActivity@4e8c368c
D/LifecycleMonitor: Lifecycle status change: androidx.activity.ComponentActivity@4e8c368c in: STOPPED
V/ActivityScenario: Update currentActivityStage to STOPPED, currentActivity=androidx.activity.ComponentActivity@4e8c368c
I/ExoPlayerImpl: Release 11873dbb [AndroidXMedia3/1.1.0] [robolectric, robolectric, robolectric, 33] [media3.common, media3.exoplayer, media3.decoder]
D/LifecycleMonitor: Lifecycle status change: androidx.activity.ComponentActivity@4e8c368c in: DESTROYED
V/ActivityScenario: Update currentActivityStage to DESTROYED, currentActivity=null
I/Choreographer: Skipped 1000000 frames! The application may be doing too much work on its main thread.
test timed out after 5000 milliseconds
org.junit.runners.model.TestTimedOutException: test timed out after 5000 milliseconds
So something is blocking somewhereCLOVIS
07/24/2023, 10:06 AMJoffrey
07/24/2023, 10:06 AMreactormonk
07/24/2023, 10:08 AMrunTest
vs. runBlocking
- I explicitly changed it, so I can use delays when required.Joffrey
07/24/2023, 10:09 AMrunTest
is very good at testing delay mechanisms without actually spending that time in tests. But yes, it must be handled with care. And if you have problems like the one you have right now, I would probably switch to runBlocking
during the investigation just to check if that's not an issue in the test machinery 😆reactormonk
07/24/2023, 10:10 AMreactormonk
07/24/2023, 10:10 AMreactormonk
07/24/2023, 1:38 PMSup4+
val composeTestRule = createComposeRule(CoroutineName("Test") + Dispatchers.Default)
Joffrey
07/24/2023, 1:46 PMLaunchedEffect
launches your code in some coroutine scope. I'm not familiar with Compose, especially on Android, so I can't elaborate on that, but that scope might have a single-threaded context. If you use this same scope in multiple places, and some things do block the thread, then it may prevent other unrelated coroutines from running. You should make sure that calls that are potentially blocking are called on the IO
dispatcher to avoid this kind of starvation.
From what I can see with your change, you're overriding the context used by the compose component container, making it multi-threaded (because of Dispatchers.Default
), so that would solve such problemsreactormonk
07/24/2023, 1:46 PMEmptyCoroutineContext
.