-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
In the test module, collect and report the actual uncaught exceptions #3449
Conversation
A draft because it's untested and I'm not sure about the whole ANDROID_DETECTED thing (in a separate commit). @qwwdfsad, what do you think of the general approach? |
cebf6b4
to
b9c3e77
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really happy with the approach, but it seems like we have no other (reasonable) options.
I'll revisit it this week along with an evaluation of user requests
kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.kt
Outdated
Show resolved
Hide resolved
kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise LGTM
kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.kt
Outdated
Show resolved
Hide resolved
ead5c0b
to
c4345c5
Compare
e5051ba
to
7695863
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea in general.
I'll take a final look after the rename and I believe it is good to go
kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.kt
Outdated
Show resolved
Hide resolved
…utines`: common files are named as `Foo.common.kt`, while platform-specific files are named `Foo.kt`. See: Kotlin/kotlinx.coroutines#3449 (comment) Signed-off-by: Artyom Shendrik <[email protected]>
Let's finalize and merge it? |
7695863
to
b12a404
Compare
(preliminary looks good, will take a one final look when #3603 is merged) |
Could you please rebase this one on the recent |
9d51a36
to
57497ec
Compare
Sure, done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM module issues found.
While working on that, a few iterations were done, and it's easy to forget the reasning for the final solution over time. Issue fixed is also quite heavy on the discussion and potential solutions. While squashing, could you please provide an extensive commit message with the explanation of refactoring's rationale and problem solved?
kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.common.kt
Outdated
Show resolved
Hide resolved
@@ -0,0 +1 @@ | |||
kotlinx.coroutines.test.internal.ExceptionCollectorAsService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one should have the corresponding provides
in module-info.java
* The callbacks in this object are the last resort before relying on platform-dependent | ||
* ways to report uncaught exceptions from coroutines. | ||
*/ | ||
internal object ExceptionCollector: AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal object ExceptionCollector: AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { | |
internal object ExceptionCollector : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { |
@@ -251,6 +259,8 @@ internal class TestScopeImpl(context: CoroutineContext) : | |||
fun legacyLeave(): List<Throwable> { | |||
val exceptions = synchronized(lock) { | |||
check(entered && !finished) | |||
/** After [finished] becomes `true`, it is no longer valid to have [reportException] as the callback. */ | |||
ExceptionCollector.removeOnExceptionCallback(lock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like callbacks are never removed for non-legacy version, consuming the memory lineary to number of tests run, could you please address that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. You're right, of course. I simply rebased the code on develop
but totally forgot that the leave
function has split in two and needs changes in both places.
When when a `Job` doesn't have a parent, failures in it can not be reported via structured concurrency. Instead, the mechanism of unhandled exceptions is used. If there is a `CoroutineExceptionHandler` in the coroutine context or registered as a `ServiceLoader` service, this gets notified, and otherwise, something platform-specific happens: on Native, the program crashes, and on the JVM, by default, the exception is just logged, though this is configurable via `Thread.setUncaughtExceptionHandler`. With tests on the JVM, this is an issue: we want exceptions not simply *logged*, we want them to fail the test, and this extends beyond just coroutines. However, JUnit does not override the uncaught exception handler, and uncaught exceptions do just get logged: <https://stackoverflow.com/questions/36648317/how-to-capture-all-uncaucht-exceptions-on-junit-tests> This is a problem with a widely-used `viewModelScope` on Android. This is a scope without a parent, and so the exceptions in it are uncaught. On Android, such uncaught exceptions crash the app by default, but in tests, they just get logged. Clearly, without overriding the behavior of uncaught exceptions, the tests are lacking. This can be solved on the test writers' side via `setUncaughtExceptionHandler`, but one has to remember to do that. In this commit, we attempt to solve this issue for the overwhelming majority of users. To that end, when the test framework is used, we collect the uncaught exceptions and report them at the end of a test. This approach is marginally less robust than `setUncaughtExceptionHandler`: if an exception happened after the last test using `kotlinx-coroutines-test`, it won't get reported, for example. `CoroutineExceptionHandler` is designed in such a way that, when it is used in a coroutine context, its presence means that the exceptions are safe in its care and will not be propagated further, but when used as a service, it has no such property. We, however, know that our `CoroutineExceptionHandler` reports the exceptions properly and they don't need to be further logged, and so we had to extend the behavior of mechanism for uncaught exception handling so that it the handler throws a new kind of exception if the exception was processed successfully. Also, because there's no `ServiceLoader` mechanism on JS or Native, we had to refactor the whole uncaught exception handling mechanism a bit in the same vein as we had to adapt the `Main` dispatcher to `Dispatchers.setMain`: by introducing internal setter APIs that services have to call manually to register in. Fixes #1205 as thoroughly as we can given the circumstances.
2d5008a
to
4fda39a
Compare
I squashed the commits into one and wrote a detailed commit message. Did you have something like that in mind? |
Exactly, nicely done! |
With #3449, the unrelated uncaught exceptions can also be caught in this manner.
An attempt to mitigate #1205.