fabriciorby / maven-surefire-junit5-tree-reporter

Tree view console report for Maven Surefire JUnit5 plugin. :^)
Apache License 2.0
181 stars 27 forks source link

IndexOutOfBoundsException being dumped #29

Open ascopes opened 1 year ago

ascopes commented 1 year ago

Just had a look in my Surefire reports output directory and spotted the following:

target/surefire-reports/2022-12-30T14-01-27_616-jvmRun1.dumpstream

# Created at 2022-12-30T14:01:38.848
ForkStarter IOException: Index: 1, Size: 1.
org.apache.maven.plugin.surefire.booterclient.output.MultipleFailureException: Index: 1, Size: 1
    at org.apache.maven.plugin.surefire.booterclient.output.ThreadedStreamConsumer$Pumper.<init>(ThreadedStreamConsumer.java:64)
    at org.apache.maven.plugin.surefire.booterclient.output.ThreadedStreamConsumer.<init>(ThreadedStreamConsumer.java:122)
    at org.apache.maven.plugin.surefire.booterclient.ForkStarter.fork(ForkStarter.java:600)
    at org.apache.maven.plugin.surefire.booterclient.ForkStarter.run(ForkStarter.java:311)
    at org.apache.maven.plugin.surefire.booterclient.ForkStarter.run(ForkStarter.java:268)
    at org.apache.maven.plugin.surefire.AbstractSurefireMojo.executeProvider(AbstractSurefireMojo.java:1334)
    at org.apache.maven.plugin.surefire.AbstractSurefireMojo.executeAfterPreconditionsChecked(AbstractSurefireMojo.java:1167)
    at org.apache.maven.plugin.surefire.AbstractSurefireMojo.execute(AbstractSurefireMojo.java:931)
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:137)
    at org.apache.maven.lifecycle.internal.MojoExecutor.doExecute2(MojoExecutor.java:370)
    at org.apache.maven.lifecycle.internal.MojoExecutor.doExecute(MojoExecutor.java:351)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:215)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:171)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:163)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:117)
    at org.apache.maven.lifecycle.internal.builder.multithreaded.MultiThreadedBuilder$1.call(MultiThreadedBuilder.java:210)
    at org.apache.maven.lifecycle.internal.builder.multithreaded.MultiThreadedBuilder$1.call(MultiThreadedBuilder.java:195)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:577)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    at java.base/java.lang.Thread.run(Thread.java:1589)
    Suppressed: java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
        at java.base/java.util.Collections$SingletonList.get(Collections.java:4959)
        at org.apache.maven.plugin.surefire.report.TreePrinter$TestPrinter.printClass(TreePrinter.java:162)
        at org.apache.maven.plugin.surefire.report.TreePrinter$TestPrinter.printTest(TreePrinter.java:96)
        at org.apache.maven.plugin.surefire.report.TreePrinter$TestPrinter.access$500(TreePrinter.java:85)
        at org.apache.maven.plugin.surefire.report.TreePrinter.lambda$printTests$1(TreePrinter.java:82)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
        at org.apache.maven.plugin.surefire.report.TreePrinter.printTests(TreePrinter.java:82)
        at org.apache.maven.plugin.surefire.report.TestReportHandler.printTests(TestReportHandler.java:111)
        at org.apache.maven.plugin.surefire.report.TestReportHandler.print(TestReportHandler.java:44)
        at org.apache.maven.plugin.surefire.report.ConsoleTreeReporterBase.testSetCompleted(ConsoleTreeReporterBase.java:26)
        at org.apache.maven.plugin.surefire.report.ConsoleTreeReporterAscii.testSetCompleted(ConsoleTreeReporterAscii.java:32)
        at org.apache.maven.plugin.surefire.report.ConsoleTreeReporterBase.testSetCompleted(ConsoleTreeReporterBase.java:8)
        at org.apache.maven.plugin.surefire.report.TestSetRunListener.testSetCompleted(TestSetRunListener.java:213)
        at org.apache.maven.plugin.surefire.booterclient.output.ForkClient$TestSetCompletedListener.handle(ForkClient.java:147)
        at org.apache.maven.plugin.surefire.booterclient.output.ForkClient$TestSetCompletedListener.handle(ForkClient.java:135)
        at org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessEventNotifier.notifyEvent(ForkedProcessEventNotifier.java:233)
        at org.apache.maven.plugin.surefire.booterclient.output.ForkClient.handleEvent(ForkClient.java:353)
        at org.apache.maven.plugin.surefire.booterclient.output.ForkClient.handleEvent(ForkClient.java:59)
        at org.apache.maven.plugin.surefire.booterclient.output.ThreadedStreamConsumer$Pumper.run(ThreadedStreamConsumer.java:96)
        ... 1 more

Tests appear to still be running, though. I don't know how serious this is or whether it is anything to worry about or not.


Other info:

Invocation: $ ./mvnw clean verify -T8

$ uname -a
Linux z170x-fedora 6.0.12-300.fc37.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 8 16:58:47 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

$ java -version
java -version
openjdk version "19.0.1" 2022-10-18
OpenJDK Runtime Environment Corretto-19.0.1.10.1 (build 19.0.1+10-FR)
OpenJDK 64-Bit Server VM Corretto-19.0.1.10.1 (build 19.0.1+10-FR, mixed mode, sharing)

$ ./mvnw --version 
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: /home/ashley/.m2/wrapper/dists/apache-maven-3.8.6-bin/1ks0nkde5v1pk9vtc31i9d0lcd/apache-maven-3.8.6
Java version: 19.0.1, vendor: Amazon.com Inc., runtime: /home/ashley/.sdkman/candidates/java/19-amzn
Default locale: en_GB, platform encoding: UTF-8
OS name: "linux", version: "6.0.12-300.fc37.x86_64", arch: "amd64", family: "unix"
<properties>
  <argLine>
    -Dorg.slf4j.simpleLogger.log=INFO
    -Xshare:off
  </argLine>

  <maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
  <maven-surefire-junit5-tree-reporter.version>1.1.0</maven-surefire-junit5-tree-reporter.version>
</properties>
<plugin>
  <!-- Unit testing config -->
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>${maven-surefire-plugin.version}</version>

  <configuration>
    <excludes>
      <exclude>*IntegrationTest</exclude>
    </excludes>

    <failIfNoTests>true</failIfNoTests>
    <runOrder>random</runOrder>
    <tempDir>./tmp/unit</tempDir>
    <useModulePath>true</useModulePath>

    <!--
      This block is needed to show @DisplayName and @ParameterizedTest
      in reports with the provided names.
    -->
    <statelessTestsetReporter
      implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
      <disable>false</disable>
      <version>3.0</version>
      <usePhrasedFileName>false</usePhrasedFileName>
      <usePhrasedTestSuiteClassName>true</usePhrasedTestSuiteClassName>
      <usePhrasedTestCaseClassName>true</usePhrasedTestCaseClassName>
      <usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
    </statelessTestsetReporter>
    <consoleOutputReporter>
      <disable>${hide-test-logs-in-console}</disable>
    </consoleOutputReporter>
    <statelessTestsetInfoReporter
      implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter">
      <disable>false</disable>
      <usePhrasedFileName>false</usePhrasedFileName>
      <usePhrasedClassNameInRunning>true</usePhrasedClassNameInRunning>
      <usePhrasedClassNameInTestCaseSummary>true</usePhrasedClassNameInTestCaseSummary>
    </statelessTestsetInfoReporter>
  </configuration>

  <dependencies>
    <dependency>
      <groupId>me.fabriciorby</groupId>
      <artifactId>maven-surefire-junit5-tree-reporter</artifactId>
      <version>${maven-surefire-junit5-tree-reporter.version}</version>
    </dependency>
  </dependencies>
</plugin>

src/test/resources/junit-platform.properties

junit.jupiter.execution.parallel.enabled=true
ascopes commented 1 year ago

Since I am not sure of the conditions causing this issue, it is hard to make a minimal working reproduction. However, I seem to be able to reproduce this by in my project. Should be able to trigger it with:

$ git clone git@github.com:ascopes/java-compiler-testing
$ cd java-compiler-testing
$ git checkout 511f1e87d4ff6316ac8330bb800e16c707d83953
$ ./mvnw clean test -am -e -pl java-compiler-testing -T4
fabriciorby commented 1 year ago

Since I am not sure of the conditions causing this issue, it is hard to make a minimal working reproduction. However, I seem to be able to reproduce this by in my project. Should be able to trigger it with:

$ git clone git@github.com:ascopes/java-compiler-testing
$ cd java-compiler-testing
$ git checkout 511f1e87d4ff6316ac8330bb800e16c707d83953
$ ./mvnw clean test -am -e -pl java-compiler-testing -T4

I followed these steps and I was not able to reproduce :/

Not so long ago I've opened an issue in the SUREFIRE Jira and for some reason it looks like that the <exclude> tag can change the behavior of how the tests are printed. (https://issues.apache.org/jira/browse/SUREFIRE-2111?page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel&focusedCommentId=17652515#comment-17652515)

Could you do a favor, please? Could you try to run the same test, but without the <exclude> and see what happens?

ascopes commented 1 year ago

The first 15 or so times I tried this tonight, it seems to have not had the issue. I spotted one run after this that did have it and it would appear to be to do with the ordering of tests.

Looks like passing -Dsurefire.runOrder.random.seed=718860759208 to the Maven invocation makes this occur each time (might be a red herring, but one to try perhaps?)

Just about to try without exclude.

ascopes commented 1 year ago

Without the exclude, it doesn't seem to do it with that above seed. Just rerunning a few runs with random order again as unsetting the <exclude> in my project will cause several integration tests to run which may mess with test ordering. Will also try and attach a debugger to see if I can get any info as to what it is processing when it fails and get back to you.

ascopes commented 1 year ago

Ok, so I attached a debugger and I can see that testResult.getSourceName() is returning io.github.ascopes.jct.tests.unit.compilers.AbstractJctCompilerTest$TargetTests. The getTreeLength is going to return 1 from this, I assume.

The class results list itself only has the one element though:

image

...so the .get(1) is going to fail there.

I managed to debug this by creating mvnwDebug scripts (./mvnw wrapper:wrapper -DincludeDebug=true), and then running ./mvnwDebug -Dsurefire.runOrder.random.seed=718860759208 -T4 -pl java-compiler-testing -am test on git ref 511f1e87d4ff6316ac8330bb800e16c707d83953 as above.

I attached a remote debugger on port 8000 from IntelliJ once I ran this, and added an exception break point on any IndexOutOfBoundsException getting raised. I then manually added the junit5-tree-reporter plugin to my POM while the debugger was running and instructed IntelliJ to refresh the project dependencies so I could get the source code for the plugin while the debugger stopped on it.

Looks like it definitely might be related to what you suggested. I am guessing this is making the assumption that the outer class runs tests before the inner class or something. The bit I am a bit confused on is that the data in classResults shows a completely different test class to the one in testResult.

image

Could it be a race condition somewhere perhaps? The call to printTests:111 in TestReportHandler show at least two different test classes in the list of tests being rendered.

image

get:4959, Collections$SingletonList (java.util)
printClass:162, TreePrinter$TestPrinter (org.apache.maven.plugin.surefire.report)
printTest:96, TreePrinter$TestPrinter (org.apache.maven.plugin.surefire.report)
access$500:85, TreePrinter$TestPrinter (org.apache.maven.plugin.surefire.report)
lambda$printTests$1:82, TreePrinter (org.apache.maven.plugin.surefire.report)
accept:-1, TreePrinter$$Lambda$247/0x000000080145e230 (org.apache.maven.plugin.surefire.report)
accept:183, ForEachOps$ForEachOp$OfRef (java.util.stream)
accept:197, ReferencePipeline$3$1 (java.util.stream)
forEachRemaining:1625, ArrayList$ArrayListSpliterator (java.util)
copyInto:509, AbstractPipeline (java.util.stream)
wrapAndCopyInto:499, AbstractPipeline (java.util.stream)
evaluateSequential:150, ForEachOps$ForEachOp (java.util.stream)
evaluateSequential:173, ForEachOps$ForEachOp$OfRef (java.util.stream)
evaluate:234, AbstractPipeline (java.util.stream)
forEach:596, ReferencePipeline (java.util.stream)
printTests:82, TreePrinter (org.apache.maven.plugin.surefire.report)
printTests:111, TestReportHandler (org.apache.maven.plugin.surefire.report)
print:44, TestReportHandler (org.apache.maven.plugin.surefire.report)
testSetCompleted:26, ConsoleTreeReporterBase (org.apache.maven.plugin.surefire.report)
testSetCompleted:32, ConsoleTreeReporterUnicode (org.apache.maven.plugin.surefire.report)
testSetCompleted:8, ConsoleTreeReporterBase (org.apache.maven.plugin.surefire.report)
testSetCompleted:213, TestSetRunListener (org.apache.maven.plugin.surefire.report)
handle:147, ForkClient$TestSetCompletedListener (org.apache.maven.plugin.surefire.booterclient.output)
handle:135, ForkClient$TestSetCompletedListener (org.apache.maven.plugin.surefire.booterclient.output)
notifyEvent:233, ForkedProcessEventNotifier (org.apache.maven.plugin.surefire.booterclient.output)
handleEvent:353, ForkClient (org.apache.maven.plugin.surefire.booterclient.output)
handleEvent:59, ForkClient (org.apache.maven.plugin.surefire.booterclient.output)
run:96, ThreadedStreamConsumer$Pumper (org.apache.maven.plugin.surefire.booterclient.output)
run:1589, Thread (java.lang)

(☝🏻 stacktrace within the IntelliJ debugger)

I'm off to bed for the night now, but if I have time tomorrow and there is any other info you need, I'll try and find it out and let you know.

Thanks for the quick response!

fabriciorby commented 1 year ago

Hey, thanks for all the effort with debugging!

I just discovered this issue with <exclude> 5 days ago, the ticket was raised exactly while I was developing some features in July. So using <exclude> is absolutely untested and will get problems with Parameterized and Nested tests (afaik).

Unfortunately, there's no easy fix and I will need some more time to analyze what can be done. Of course, you're very welcome to help if you want.

ascopes commented 1 year ago

Cool, no worries. I may be able to look at some stuff at the weekend if I have time, so feel free to shout if there is anything in particular.

aukevanleeuwen commented 6 months ago

Could you do a favor, please? Could you try to run the same test, but without the and see what happens?

FYI: I have this exact same error, but I don't have any <excludes />. I do have <includes /> explicitly though, so maybe that's different?

I'm using this reporter in a maven-failsafe-plugin configuration though, not a surefire one, but I think that plugin code is essentially the same.

aukevanleeuwen commented 6 months ago

Since the earlier referenced JIRA issue mentions something about @Nested classes, I tried a couple of times and I cannot reproduce it if I don't use the @Nested. I can reproduce it if I do use it, so that's a pointer of where it's going wrong.

Adding -Dfailsafe.excludes= or changing that configuration to have <excludes><exclude /></excludes> doesn't help by the way.

So I can reliably reproduce, but can't share the code as it is. I will try to debug it a bit as well.

aukevanleeuwen commented 6 months ago

I can't say I dove into it so much that I really understand everything that happening, but in the end it goes wrong in the following location:

https://github.com/fabriciorby/maven-surefire-junit5-tree-reporter/blob/45484196182a1de41cbb5da699a24d7086d8da3a/src/main/java/org/apache/maven/plugin/surefire/report/TreePrinter.java#L232

Where treeLength is calculated here upon creation of the TreePrinter here:

https://github.com/fabriciorby/maven-surefire-junit5-tree-reporter/blob/45484196182a1de41cbb5da699a24d7086d8da3a/src/main/java/org/apache/maven/plugin/surefire/report/TreePrinter.java#L257-L263

Again I didn't dive into it very deeply (don't have the time for that), but I don't understand why you would calculate your own treeLength based on the number of $ characters in the class name. Wouldn't it be roughly the same as just setting treeLength to classResults.size() - 1? Then you should be always safe or am I missing something?

-- twenty minutes later --

I'm probably missing something, there is a bunch more stuff that you do to make nested tests work correctly but apparently there are some assumptions about in what order nested classes are processed, which is not the case in this parallel scenario.

One pointer perhaps that might help you is that in this block of code:

https://github.com/fabriciorby/maven-surefire-junit5-tree-reporter/blob/45484196182a1de41cbb5da699a24d7086d8da3a/src/main/java/org/apache/maven/plugin/surefire/report/TestReportHandler.java#L38-L45

I end up in the else case where it's failing whereas maybe you would have expected it to fall into the if.

--- some more time later ---

I give up for now. I'll leave this as a reference for which paths I have tried to debug, maybe it helps, maybe it doesn't. It seems like in the end there is a hidden assumption somewhere that if you have a @Nested class within another class that top level class will at least be done 'first', but this doesn't seem to be the case. So maybe it does indeed seem related to the JIRA issue mentioned earlier, but the workaround does work for me. In the end I'm running concurrent tests and it seems that JUnit has no issues running the nested class even before it has executed a test in the 'parent' class. Could that be the direction that you need to reproduce this scenario?

I fully realise that this is all open source and the work and I can't really expect you to fix it, but you have a better idea of where it might go wrong in that case. Obviously I'm grateful for the work you have put in, but if you find some time it would be awesome if this could be fixed for the greater community.

aukevanleeuwen commented 6 months ago

Another thing that I've noticed that is odd, is that when using this plugin and having @Disabled tests, those tests are duplicated in the output (?). Again just jotting it down here, because maybe it triggers something with the original author @fabriciorby...

fabriciorby commented 6 months ago

hey @aukevanleeuwen, bedankt voor je hulp.

it's correct, there are many things I needed to just assume that would occur from the surefire part, or that I had to debug surefire in order to do it

oh yes, it's very annoying to debug this code, because it is attached in maven, and you have to debug the maven goal, in my small test suite it was working, but after it got slightly popular I realized there are many edge cases that are all screwed, and some I don't know if it's even possible to be fixed

thanks for spending a considerable time trying to figure it out, I really want to take some time and follow on these bugs

I was also wanting to rewrite it to be parsed in a better Tree data structure, the way it's right now is kind of gross, but the thing is it's been a long time and I also need some time to understand once again how this stuff works :/

but now it's summer time here in the Netherlands, I'll dig into it in some months when the weather gets depressive again

thanks again, it sparkled me some motivation