close
The Wayback Machine - https://web.archive.org/web/20211229081216/https://github.com/square/leakcanary/issues/1986
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

Leakcanary makes test always pass when using androidx.test >=1.3.1-alpha02 #1986

Open
mateuszkwiecinski opened this issue Nov 2, 2020 · 16 comments

Comments

@mateuszkwiecinski
Copy link
Contributor

@mateuszkwiecinski mateuszkwiecinski commented Nov 2, 2020

Description

When using leakcanary with android-test 1.3.1-alpha02 ui test suite always pass.
Using alpha version is required due to android/android-test#743 but in case when it gets into stable release more people will face the issue 😞

Steps to Reproduce

Sample project that has test suite passing despite it shouldn't
mateuszkwiecinski/orchestrator_doesnt_work#7

image

The repository contains simple ui tests setup with orchestrator and leakcananry enabled

Expected behavior: I expect to see test failure and have failing workflow (the test shouldFail should fail)

Version Information

  • LeakCanary version: 2.5
  • Android OS version: tested 26, 29, 30
  • Gradle version: tested on 6.6.1 and 6.7

Additional Information

When looking at logcat it points at leakcanary:

2020-11-02 11:05:03.546 470-540/com.package.debug E/TestRunner: ----- begin exception -----
2020-11-02 11:05:03.546 470-540/com.package.debug E/TestRunner: kotlin.UninitializedPropertyAccessException: lateinit property testResultPublisher has not been initialized
        at leakcanary.FailTestOnLeakRunListener.testFinished(FailTestOnLeakRunListener.kt:134)
        at org.junit.runner.notification.SynchronizedRunListener.testFinished(SynchronizedRunListener.java:87)
        at org.junit.runner.notification.RunNotifier$9.notifyListener(RunNotifier.java:225)
        at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:72)
        at org.junit.runner.notification.RunNotifier.fireTestFinished(RunNotifier.java:222)
        at org.junit.internal.runners.model.EachTestNotifier.fireTestFinished(EachTestNotifier.java:38)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:372)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
        at org.junit.runners.Suite.runChild(Suite.java:128)
        at org.junit.runners.Suite.runChild(Suite.java:27)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
        at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
        at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:434)
        at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2205)
2020-11-02 11:05:03.546 470-540/compackage.debug E/TestRunner: ----- end exception -----
@opatry
Copy link
Contributor

@opatry opatry commented Nov 7, 2020

lateinit var are so Java's NPE 1�7 I tend to avoid it and simply rely on nullable far more explicit!

I suggest to make testResultPublisher nullable then.
Otherwise, check on this::testResultPublisher.isInitialized could be done but meehh 1�7

opatry added a commit to opatry/leakcanary that referenced this issue Nov 7, 2020
@opatry
Copy link
Contributor

@opatry opatry commented Nov 7, 2020

@pyricau unless this proposal is considered too defensive and this state isn't expected when executing RunListener implementation?

Is it something possible to have testRunStarted not called while failTest (called by detectLeaks from testFinished) is?

@opatry
Copy link
Contributor

@opatry opatry commented Nov 7, 2020

@pyricau do you have a recipe somewhere to use leakcanary from sources in an external project, like the one provided in this issue? Currently, I tweaked a lot (both on leakcanary and example repositories) to make it work.
So, I guess I'm missing something obvious 😅

@opatry
Copy link
Contributor

@opatry opatry commented Nov 7, 2020

The real issue causing trouble is the following exception, not kotlin.UninitializedPropertyAccessException: lateinit property testResultPublisher has not been initialized when I try with my setup:

2020-11-07 22:10:32.129 14094-14119/com.example.myapplication E/TestRunner: failed: Test mechanism
2020-11-07 22:10:32.129 14094-14119/com.example.myapplication E/TestRunner: ----- begin exception -----
2020-11-07 22:10:32.130 14094-14119/com.example.myapplication E/TestRunner: java.lang.NoSuchFieldException: No field orchestratorListener in class Landroidx/test/runner/AndroidJUnitRunner; (declaration of 'androidx.test.runner.AndroidJUnitRunner' appears in /data/app/~~CPf-cpXbvJK3gl7Eclskvg==/com.example.myapplication.test-IBmvTiaApQAoie0dWx3Nyg==/base.apk)
        at java.lang.Class.getDeclaredField(Native Method)
        at leakcanary.internal.TestResultPublisher$Companion.install(TestResultPublisher.kt:22)
        at leakcanary.FailTestOnLeakRunListener.testRunStarted(FailTestOnLeakRunListener.kt:63)
        at org.junit.runner.notification.SynchronizedRunListener.testRunStarted(SynchronizedRunListener.java:35)
        at org.junit.runner.notification.RunNotifier$1.notifyListener(RunNotifier.java:91)
        at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:72)
        at org.junit.runner.notification.RunNotifier.fireTestRunStarted(RunNotifier.java:88)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:136)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
        at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
        at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:434)
        at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2205)
2020-11-07 22:10:32.130 14094-14119/com.example.myapplication E/TestRunner: ----- end exception -----
2020-11-07 22:10:32.131 14094-14119/com.example.myapplication E/OrchestrationListener: Unable to determine test case from description [No Tests]
    androidx.test.services.events.TestEventException: Unexpected description instance: No Tests
        at androidx.test.services.events.ParcelableConverter.getTestCaseFromDescription(ParcelableConverter.java:49)
        at androidx.test.internal.events.client.OrchestratedInstrumentationListener.getTestFailureEventFromCachedDescription(OrchestratedInstrumentationListener.java:156)
        at androidx.test.internal.events.client.OrchestratedInstrumentationListener.testFailure(OrchestratedInstrumentationListener.java:137)
        at org.junit.runner.notification.SynchronizedRunListener.testFailure(SynchronizedRunListener.java:63)
        at org.junit.runner.notification.RunNotifier$4.notifyListener(RunNotifier.java:142)
        at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:72)
        at org.junit.runner.notification.RunNotifier.fireTestFailures(RunNotifier.java:138)
        at org.junit.runner.notification.RunNotifier.access$100(RunNotifier.java:21)
        at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:78)
        at org.junit.runner.notification.RunNotifier.fireTestRunStarted(RunNotifier.java:88)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:136)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
        at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
        at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:434)
        at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2205)

I protected such error and the test is now correctly marked as failed as expected.
That being said, I don't know exactly what it means and if something more needs to be done. I guess this access by reflection wasn't made for the pleasure of doing it. Which concrete instance of AndroidJUnitRunner are you expecting that should have a orchestratorListener field?

I'll push a PR with my changes even if I don't think it will be the end of the story.

opatry added a commit to opatry/leakcanary that referenced this issue Nov 7, 2020
…TestOnLeakRunListener .testResultPublisher` (relates to square#1986)

This seems to be the cause of square#1986 but even without this fix, the issue example worked by fixing the root cause of this issue.
opatry added a commit to opatry/leakcanary that referenced this issue Nov 7, 2020
…Listener` field (fixes square#1986)

Such field might be missing, observed with `android-test 1.3.1-alpha02` at least.
@opatry
Copy link
Contributor

@opatry opatry commented Nov 7, 2020

It seems this commit android/android-test/e998fe63c1870e79548d2b6386db81ca07010c5c introduced the issue.

OrchestratedInstrumentationListener orchestratorListener is replaced by TestEventClient testEventClient.

TestEventClient is only available with androidx.test:runner:1.3.1-alpha02, if leakcanary needs to depend on it, is it a problem?
Moreover, OrchestratedInstrumentationListener package changed from androidx.test.orchestrator.instrumentationlistener to androidx.test.internal.events.client, so I guess having a strategy for old runtime and recent one might be difficult with current impl.
Might need deeper changes I'm afraid.

@pyricau
Copy link
Member

@pyricau pyricau commented Dec 2, 2020

Catching up, sorry it took me so long to take a look.

@pyricau
Copy link
Member

@pyricau pyricau commented Dec 3, 2020

do you have a recipe somewhere to use leakcanary from sources in an external project, like the one provided in this issue?

The LeakCanary example app has UI tests that fail (and don't run on CI). You could add a test there (and update the espresso version temporarily).

Based on what you're telling me, it sounds like we need a two different impls. We could check for whether the OrchestratedInstrumentationListener vs TestEventClient impls are available, and use one strategy / test impl vs another. We'd have to move those impls into two different modules that each have a different version of the dependency.

@mateuszkwiecinski
Copy link
Contributor Author

@mateuszkwiecinski mateuszkwiecinski commented Aug 4, 2021

FYI: The issue persiststs in LeakCanary 2.7 and androidx.test 1.4.0 as well.
Updated sample project: mateuszkwiecinski/orchestrator_doesnt_work#8

Surprisingly, the behavior I observe in my default project is a bit different. It doesn't make the whole test suite to pass, but it shows Leakcanary Failure as a separate Test mechanism step:

Test mechanism > null[Pixel_XL_API_28(AVD) - 9] FAILED
        java.lang.NoSuchFieldException: No field orchestratorListener in class Landroidx/test/runner/AndroidJUnitRunner; (declaration of 'androidx.test.runner.AndroidJUnitRunner' appears in /data/app/***.debug.test-FLMMcvtXSvyPQf3e3MDyYQ==/base.apk)
        at java.lang.Class.getDeclaredField(Native Method)

but yeah, basically Leakcanary Espresso intergration is broken utterly.

@mateuszkwiecinski mateuszkwiecinski changed the title Leakcanary makes test always pass when using android-test 1.3.1-alpha02 Leakcanary makes test always pass when using androidx.test >=1.3.1-alpha02 Aug 4, 2021
@dawidhyzy
Copy link

@dawidhyzy dawidhyzy commented Aug 4, 2021

I face the same issue when updating to androidx.test 1.4.0. Is there a way to disable this integration for tests?

@mateuszkwiecinski
Copy link
Contributor Author

@mateuszkwiecinski mateuszkwiecinski commented Aug 4, 2021

Is there a way to disable this integration for tests?

@dawidhyzy You had to explicitly enable it, soo the way to disable the integration is to comment out lines responsible for enabling it 😛

Note that LeakCanary is automatically disabled in tests (see Running LeakCanary in instrumentation tests)

Source: https://square.github.io/leakcanary/faq/#how-do-i-know-if-leakcanary-is-running

@mateuszkwiecinski
Copy link
Contributor Author

@mateuszkwiecinski mateuszkwiecinski commented Sep 8, 2021

It isn't linked but it seems like this issue will be fixed by #2147.

@brettchabot
Copy link

@brettchabot brettchabot commented Oct 20, 2021

androidx.test maintainer here. Please let us know how we could help migrate LeakCanary from depending on androidx.test implementation details. #2147 will only 'fix' this issue until the next time the implementation is changed again.

Anything in androidx.test.internal is not a stable API surface and no external libraries should depend on it.

@pyricau
Copy link
Member

@pyricau pyricau commented Dec 15, 2021

Thanks everybody, sorry this is taking a while.

Anything in androidx.test.internal is not a stable API surface and no external libraries should depend on it.

@brettchabot I hear you, but also: https://en.wikipedia.org/wiki/Desire_path

In other words, when an API is missing, users will find a way around it. The missing API was "I need a way to fail an otherwise successful test once it's done running". A bit like a rule but that applies to all tests

There is one API that's available: Junit rules. The problem with those is two fold: 1) they're not applied to all tests, so you have to manually add to each test. 2) Espresso itself had leaks within its outer rules code (it keeps activities after they're destroyed) and that's meant leak detection would trigger due to Espresso. I believe that's fixed now, and we could likely use a rule, but it's definitely way less convenient.

I might take a spike at migrating this and the documentation to rely on rules instead, but that's a pretty significant change.

@brettchabot
Copy link

@brettchabot brettchabot commented Dec 15, 2021

Would a custom runner work instead? https://stackoverflow.com/questions/9034812/how-to-apply-a-junit-rule-for-all-test-cases-in-a-suite

AndroidJUnitRunner supports specifying a custom runner builder via command line.

We could also consider enhancing AndroidJUnitRunner to support specifying RunnerBuilders via ServiceLoader if that would be easier. It already does this for RunListener's. That way if leak canary is on the classpath, users would automatically get its custom runner.

@pyricau
Copy link
Member

@pyricau pyricau commented Dec 16, 2021

Interesting suggestion, I need to try this out!

pyricau added a commit that referenced this issue Dec 29, 2021
This commit deprecates FailTestOnLeakRunListener, FailAnnotatedTestOnLeakRunListener, FailTestOnLeak and InstrumentationLeakDetector.

- `FailTestOnLeakRunListener` is replaced by the `DetectLeaksAfterTestSuccess` test rule.
- Test code and test utilities can also call `LeakAssertions.assertNoLeak()`, which by default throws an exception if leaks are found.
- Specific tests can make calls to `LeakAssertions.assertNoLeak()` a no-op by using the `@SkipLeakDetection` annotation (and setting up the TestDescriptionHolder test rule)
- The leak detection and reporting behavior can be customized with `DetectLeaksAssert.update()`

Fixes #1986
@pyricau
Copy link
Member

@pyricau pyricau commented Dec 29, 2021

I'm working on deprecating the run listener, providing more fine grained APIs and building back on top with rules etc. See #2244

pyricau added a commit that referenced this issue Dec 29, 2021
This commit deprecates FailTestOnLeakRunListener, FailAnnotatedTestOnLeakRunListener, FailTestOnLeak and InstrumentationLeakDetector.

- `FailTestOnLeakRunListener` is replaced by the `DetectLeaksAfterTestSuccess` test rule.
- Test code and test utilities can also call `LeakAssertions.assertNoLeak()`, which by default throws an exception if leaks are found.
- Specific tests can make calls to `LeakAssertions.assertNoLeak()` a no-op by using the `@SkipLeakDetection` annotation (and setting up the TestDescriptionHolder test rule)
- The leak detection and reporting behavior can be customized with `DetectLeaksAssert.update()`

Fixes #1986

Co-authored-by: Preetha Suraj <preetha@squareup.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants