Skip to content

Parallel concurrent execution of instrumentation tests #295

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

Closed
TheKeeperOfPie opened this issue Mar 6, 2023 · 6 comments · Fixed by #307
Closed

Parallel concurrent execution of instrumentation tests #295

TheKeeperOfPie opened this issue Mar 6, 2023 · 6 comments · Fixed by #307
Assignees
Labels

Comments

@TheKeeperOfPie
Copy link

I realize I'm using experimental APIs on top of experiment APIs, but does anyone know if this works with JUnit 5's parallel execution in concurrent mode?

The actual running of the tests seems to work just fine. If I log the end of my test methods, I can verify they all actually execute on device when running through gradlew and Studio, but the test reporter spits out Test run failed to complete. Expected 4 tests, received 1.

So maybe support is already there, but the test reporter doesn't know how to handle concurrent test results. I might also be holding it wrong.

I tried to add support here in case a repro is needed: TheKeeperOfPie/ArtistAlleyDatabase@ecb900b

You'll have to toggle the enabled property here. And execute EntryDetailsAddTest. The repo has a few setup steps, and you might also need to delete gradle/verification-metadata.xml because I've had trouble bringing it up on a fresh project with dependency verification working.

@TheKeeperOfPie
Copy link
Author

Okay, so it seems AndroidJUnit4 doesn't support reporting test results out of order. Parallel works if you edit the AndroidJUnitPlatformRunnerListener to collect RunNotifier events and only send them once a test finishes, ensuring that each test method gets all of its events reported in full before another method's events.

The downside to this is that method timing reporting is now lost, since everything "finishes" instantly. But it looks like the tests themselves do execute in parallel.

Unfortunately this requires editing a lot of hidden library code, so there's no easy patch for consumers. I might try and fork to add support, or maybe there's an easy way to detect if parallel tests are running and offer an option to do this collection logic.

This could also just be a bug in the AndroidJUnit4 reporter, but since there's no supported way to run parallel tests in the 3P world, it probably won't get fixed.

@TheKeeperOfPie
Copy link
Author

TheKeeperOfPie commented Mar 20, 2023

Actually, it might be simpler than I thought. This seems to work. I had to use the snapshot version of the plugin to avoid a builder configuration check.

android {
    defaultConfig {    
        testInstrumentationRunnerArguments["runnerBuilder"] =
            "com.example.AndroidJUnitBuilder"
    }
}
class AndroidJUnitBuilder : RunnerBuilder() {
    private val builder = AndroidJUnit5Builder()
    
    override fun runnerForClass(testClass: Class<*>): Runner? {
        val runner = builder.runnerForClass(testClass) ?: return null
        return object : Runner() {
            override fun getDescription() = runner.description
            override fun run(notifier: RunNotifier) = runner.run(ParallelRunNotifier(notifier))
        }
    }
}

class ParallelRunNotifier(private val notifier: RunNotifier) : RunNotifier() {

    class Failures(var assumptionFailure: Failure?, var testFailure: Failure?)

    private val events = mutableMapOf<Description, Failures>()
    private val lock = Any()

    override fun fireTestStarted(description: Description) {
        events[description] = Failures(null, null)
    }

    override fun fireTestFailure(failure: Failure) {
        events[failure.description]!!.testFailure = failure
    }

    override fun fireTestAssumptionFailed(failure: Failure) {
        events[failure.description]!!.assumptionFailure = failure
    }

    override fun fireTestFinished(description: Description) = synchronized(lock) {
        val event = events[description]!!
        notifier.fireTestStarted(description)
        if (event.assumptionFailure != null) {
            notifier.fireTestAssumptionFailed(event.assumptionFailure)
        } else if (event.testFailure != null) {
            notifier.fireTestFailure(event.testFailure)
        }
        notifier.fireTestFinished(description)
    }
}

@mannodermaus
Copy link
Owner

Hey, thanks for the research and apologies for the late reply. I haven't explored parallel test execution with instrumentation tests yet, but your latest snippet seems like a promising starting point. Thank you for that! I'll try it out and will let you know.

@mannodermaus mannodermaus self-assigned this Sep 18, 2023
mannodermaus added a commit that referenced this issue Sep 18, 2023
Wrap the default RunNotifier with a parallel-aware variant if JUnit Jupiter's parallel test execution is enabled.
It injects itself into the default AndroidX instrumentation and reorders the emission of test events as necessary.
This requires a hefty bit of reflective inspection, therefore guard this access via a helper class.

As for the sample app, enable parallelism for it and update the tests to demonstrate the effect of it further.
Finally, improve the error message when trying to launch a UI test (e.g. Espresso) in parallel,
since that doesn't work.

Resolves #295.
mannodermaus added a commit that referenced this issue Sep 18, 2023
Wrap the default RunNotifier with a parallel-aware variant if JUnit Jupiter's parallel test execution is enabled.
It injects itself into the default AndroidX instrumentation and reorders the emission of test events as necessary.
This requires a hefty bit of reflective inspection, therefore guard this access via a helper class.

As for the sample app, enable parallelism for it and update the tests to demonstrate the effect of it further.
Finally, improve the error message when trying to launch a UI test (e.g. Espresso) in parallel,
since that doesn't work.

Resolves #295.
mannodermaus added a commit that referenced this issue Sep 18, 2023
Wrap the default RunNotifier with a parallel-aware variant if JUnit Jupiter's parallel test execution is enabled.
It injects itself into the default AndroidX instrumentation and reorders the emission of test events as necessary.
This requires a hefty bit of reflective inspection, therefore guard this access via a helper class.

As for the sample app, enable parallelism for it and update the tests to demonstrate the effect of it further.
Finally, improve the error message when trying to launch a UI test (e.g. Espresso) in parallel,
since that doesn't work.

Resolves #295.
mannodermaus added a commit that referenced this issue Sep 18, 2023
Wrap the default RunNotifier with a parallel-aware variant if JUnit Jupiter's parallel test execution is enabled.
It injects itself into the default AndroidX instrumentation and reorders the emission of test events as necessary.
This requires a hefty bit of reflective inspection, therefore guard this access via a helper class.

As for the sample app, enable parallelism for it and update the tests to demonstrate the effect of it further.
Finally, improve the error message when trying to launch a UI test (e.g. Espresso) in parallel,
since that doesn't work.

Resolves #295.
@mannodermaus
Copy link
Owner

@TheKeeperOfPie Thanks again for being patient. I was able to put in some work here and managed to make the reporting of parallel instrumentation tests better. Please feel free to check out the results in the latest 1.4.0-SNAPSHOT and let me know if you encounter any problems.

@TheKeeperOfPie
Copy link
Author

I got around to testing this and it seems to run the tests properly (tests expected matches tests actual), but unfortunately the runner still reports that the suite failed. But if I open the test report, it shows 100% success rate, so I don't know where the disagreement comes from. It's reproducible with ./gradlew test connectedAndroidTest in my same app repo, but I'll try and get a narrow scoped reproduction when I get the chance.

Regardless, thanks for taking a look at this. At the very least this lets me remove my workaround and I can rely on manually inspecting the test report to determine if all tests passed. And Studio integration works great now.

@mannodermaus
Copy link
Owner

Thanks for the feedback! Glad to hear that it (mostly) works for you. Let me try to look into the suite failure report, it's not something I have encountered while working on this. Hopefully your sample repo can give some insight. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants