Skip to content
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

Merged
merged 1 commit into from
Feb 24, 2023

Conversation

dkhalanskyjb
Copy link
Contributor

An attempt to mitigate #1205.

@dkhalanskyjb
Copy link
Contributor Author

dkhalanskyjb commented Sep 12, 2022

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?

@dkhalanskyjb dkhalanskyjb force-pushed the TestScope-uncaught-exceptions-registry branch from cebf6b4 to b9c3e77 Compare September 12, 2022 14:57
Copy link
Collaborator

@qwwdfsad qwwdfsad left a 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

Copy link
Collaborator

@qwwdfsad qwwdfsad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM

@dkhalanskyjb dkhalanskyjb force-pushed the TestScope-uncaught-exceptions-registry branch from ead5c0b to c4345c5 Compare December 16, 2022 12:31
@dkhalanskyjb dkhalanskyjb force-pushed the TestScope-uncaught-exceptions-registry branch from e5051ba to 7695863 Compare December 16, 2022 15:11
@dkhalanskyjb
Copy link
Contributor Author

@qwwdfsad, WDYT about this big rework? This is much less intrusive than the previous version, but it is still a bit unpleasant due to CoroutineExceptionHandler not being flexible enough. I described my thoughts about the underlying issue with CoroutineExceptionHandler here: #3554

@dkhalanskyjb dkhalanskyjb requested review from qwwdfsad and removed request for qwwdfsad December 27, 2022 14:24
Copy link
Collaborator

@qwwdfsad qwwdfsad left a 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

amal added a commit to fluxo-kt/fluxo that referenced this pull request Jan 6, 2023
…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 <artyom.shendrik@gmail.com>
@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Feb 6, 2023

Let's finalize and merge it?

@dkhalanskyjb dkhalanskyjb force-pushed the TestScope-uncaught-exceptions-registry branch from 7695863 to b12a404 Compare February 9, 2023 14:49
@qwwdfsad
Copy link
Collaborator

(preliminary looks good, will take a one final look when #3603 is merged)

@qwwdfsad
Copy link
Collaborator

Could you please rebase this one on the recent develop?

@dkhalanskyjb dkhalanskyjb force-pushed the TestScope-uncaught-exceptions-registry branch 2 times, most recently from 9d51a36 to 57497ec Compare February 22, 2023 11:21
@dkhalanskyjb
Copy link
Contributor Author

Sure, done.

Copy link
Collaborator

@qwwdfsad qwwdfsad left a 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?

@@ -0,0 +1 @@
kotlinx.coroutines.test.internal.ExceptionCollectorAsService
Copy link
Collaborator

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
Copy link
Collaborator

@qwwdfsad qwwdfsad Feb 22, 2023

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?

Copy link
Contributor Author

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.
@dkhalanskyjb dkhalanskyjb force-pushed the TestScope-uncaught-exceptions-registry branch from 2d5008a to 4fda39a Compare February 23, 2023 13:06
@dkhalanskyjb
Copy link
Contributor Author

I squashed the commits into one and wrote a detailed commit message. Did you have something like that in mind?

@qwwdfsad
Copy link
Collaborator

Exactly, nicely done!

@dkhalanskyjb dkhalanskyjb merged commit 1245d7e into develop Feb 24, 2023
@dkhalanskyjb dkhalanskyjb deleted the TestScope-uncaught-exceptions-registry branch February 24, 2023 17:45
dkhalanskyjb added a commit that referenced this pull request May 2, 2023
With #3449, the unrelated uncaught exceptions can also be caught in
this manner.
dkhalanskyjb added a commit that referenced this pull request May 3, 2023
…3733)

With #3449, the unrelated uncaught exceptions can also be caught in
this manner.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants