Trying to understand why the JS vs JVM behaviour i...
# test
m
Trying to understand why the JS vs JVM behaviour is different when async coroutines are launched:
Copy code
@Test
  fun test() = runTest {
    CoroutineScope(Dispatchers.Default).launch {
      delay(10.seconds)
      println("done")
    }
  }
JVM: completes immediately, without printing "done". JS: takes 10s and prints done. Is there a fundamental reason this is like this? Could JS just ignore the launched coroutine? (Or if it's a problem because it has side effects for other tests, shouldn't the bahaviour be the same for JVM?)
đź‘€ 1
o
You're launching a coroutine in
GlobalScope
. So there is no coroutine parent waiting for it. On the JVM, it most probably will land in a second thread which is terminated silently when your (only) test completes and the JVM exits. On JS, it basically works the same coroutine-wise, but all on a single thread. I'd guess that the JS engine's scheduler is waiting for all async jobs to complete (up to some timeout). I'm working on an experimental test framework which is treats coroutines as first-class citizens. I've tried your example on that one and got the same results you had.
m
I get the threading difference but even with one JS thread, you could still have unresolved promises when the test promise completes. > I'd guess that the JS engine's scheduler is waiting for all async jobs to complete ... > Something is definitely waiting. My question is why. Why the difference?
Maybe the test framework launches a new js engine every time and the js engine awaits all promises before shutting down?
o
Maybe the test framework launches a new js engine every time
Yes, a Gradle
jsNodeTest
or
jsBrowserTest
invocation makes KGP fire up a JS engine running Mocha (on the browser via Karma), which runs the tests.
and the js engine awaits all promises before shutting down?
That's what I suspect: The JS engine just enqueues your coroutine in GlobalScope on its event loop. It will terminate only after all "tasks" (JS lingo) on the event loop have completed. I agree that it is not consistent across platforms, but does it have to be? What's your intention in writing the above code in contrast to, say, something like this?
Copy code
@Test
  fun test() = runTest {
    withContext(Dispatchers.Default) {
      launch {
        delay(10.seconds)
        println("done")
      }
    }
  }
m
makes KGP fire up a JS engine running Mocha
But is it fired for each and every test? (after thinking a bit...) I thought it would wait for each test but maybe not actually, it just wait at the very end, I’ll double check that.
What’s your intention in writing the above code in contrast to, say, something like this?
I’m testing a network client that launches coroutines for background work. Those coroutines usually timeout and get collected. But if each test now has to wait for them, it makes the test ultra long (but maybe it’s not each and every test, I’ll double check that)
o
There is just one JS/Mocha invocation per Gradle task (per module), not one per test. I'll bet it just waits at the end. On the JVM, if you add more tests which together take longer than 10 seconds, I'd expect to see "done" there as well.
nod 1
👍 1
Could you just make everything run on virtual time, including your "background" coroutines?
m
Ah, that’s something else, I’m not a huge fan of the virtual time. Sometimes I actually need to test the delays
I’ll bet it just waits at the end.
Yup, that’s it. I guess there’s no such thing as a “daemon” Promise
That settles it. Thanks for the discussion!
👍 1
o
I guess there’s no such thing as a “daemon” Promise
Probably not. The JS folks would expect you to use Web Workers for that.
👍 1
On another note, because of me prototyping a test framework. This would work without waiting on all platforms. Would you consider something like this useful?
Copy code
val BackgroundJobTests by suite {
    // Register a lambda wrapping around all tests within this suite
    aroundAll { testActions ->
        withContext(Dispatchers.Default) {
            val backgroundJob = launch {
                delay(10.seconds)
                println("done")
            }
            testActions() // execute tests here 
            backgroundJob.cancel()
        }
    }

    test("test1") {
        delay(1.seconds)
    }

    test("test2") {
        delay(2.seconds)
    }
}
đź‘€ 1
m
That’d work although I’ll probably need access to
backgroundJob
in
test1
, etc...
This whole problem is just me being lazy about doing
resource.use {}
in tests so it’s not a big deal either. I can do my hoework and cleanup after each test
It’s probably better anyways for test isolation, etc...
o
> That’d work although I’ll probably need access to
backgroundJob
in
test1
, etc... Then it would probably look more like this (will finish immediately, unless you uncomment the line with
await
):
Copy code
val BackgroundJobTests by suite {
    val myThing = fixture {
        @OptIn(DelicateCoroutinesApi::class)
        GlobalScope.async {
            delay(10.seconds)
            "done"
        }
    } closeWith {
        cancel()
    }

    test("test1") {
        delay(1.seconds)
        // println(myThing().await())
    }

    test("test2") {
        delay(2.seconds)
    }
}
(Edited: Specifying
Dispatchers.Default
would be unnecessary, as coroutines would run there by default, even in tests. Using the test dispatcher is optional.)
c
I’m testing a network client that launches coroutines for background work.
Do note that Kotlinx-coroutines-test has
backgroundScope
specifically for that
👍 1
o
Yep, that's also available in my prototype as
testScope.backgroundScope
. Useful when background jobs have a single-test lifetime. When background jobs are shared across tests, you'd use the above approach.