Skip to content

Conversation

@ratcashdev
Copy link
Contributor

@ratcashdev ratcashdev commented Apr 17, 2022

Adds support for nested Junit5 test classes (@Nested) for the following operations:

  1. [MAVEN] Navigate from the Test results to a method inside a nested class - for Gradle this has already been fixed in the past.
  2. [MAVEN | GRADLE] Execute a single test method from a nested test class
  3. [MAVEN] Distinguish method names inside embedded classes with their enclosing type
  4. [MAVEN] Test file action now executes all test methods, including those in the embedded classes

Implements most of #3975.

IMO, the outstanding item is GRADLE Test Results showing all the test methods in the UI.

@ratcashdev
Copy link
Contributor Author

@neilcsmith-net please route this to the appropriate person. Thanks.

@ratcashdev
Copy link
Contributor Author

@lkishalmi can you please have a look on this - has some graddle related stuff too? Thanks.

@lkishalmi lkishalmi added this to the NB15 milestone May 4, 2022
@lkishalmi
Copy link
Contributor

The Gradle stuff looks fine for me.

@matthiasblaesing
Copy link
Contributor

I'm missing an evaluation why it is ok to change (break?) the SPI and a documentation of the change.

These are the modules, that import that single class SingleMethod:

apisupport/apisupport.ant
enterprise/j2ee.clientproject
enterprise/j2ee.ejbjarproject
enterprise/web.project
extide/gradle
groovy/groovy.support
groovy/maven.groovy
ide/gsf.testrunner.ui
ide/projectapi
java/ant.freeform
java/gradle.java
java/gradle.test
java/java.api.common
java/java.j2seproject
java/java.lsp.server
java/java.mx.project
java/junit.ant
java/junit.ant.ui
java/junit.ui
java/maven
java/maven.junit.ui
java/testng.ant
java/testng.ui
php/php.project

Will they break? Why not?

@ratcashdev
Copy link
Contributor Author

IMO SingleMethod was extended, not changed. It should be backward compatible. At least I tried to do it like that. The modules you listed are using the 2-arg constructor, in which case the behavior is as it was before.

@matthiasblaesing
Copy link
Contributor

This still needs to be documented and you did not address my question. Third-party modules could rely on the original definition of SingleMethod why are they not affected by your change? If I see it correctly SingleMethod instances coming from the modules you modified can't be equal to SingleMethod instances by untouched modules. This effectively breaks interoperability of the modules and thus my expectations of an SPI.

@ratcashdev
Copy link
Contributor Author

@matthiasblaesing I am probably not qualified to provide any explanation. I know practically zero about netbeans - just tried to scratch my own itch (and I've been using these changes daily, for 2 weeks now).

In any case it seemed to me, that the instance of SingleMethod class is not stored in some long-lived collections anyway, just used as a DTO. It's put into a TestMethod instance, which is add-ed to a List... and then the whole list is discarded. But maybe I am missing something here.

@matthiasblaesing
Copy link
Contributor

I see several modules from the java area in the list of affected modules. If any of these creates SingleMethod instances and receives one from one of the modules you modified, they will not be equals. Consider a Method Dummy#test in the file Dummy.java, you create:

SingleMethod(file=Dummy.java, methodName=test, enclosingType=Dummy)

but the receiving module might create this:

SingleMethod(file=Dummy.java, methodName=test, enclosingType=null)

According to the implementation of equals these two are not equal anymore.

This becomes even harder because the relationship between the filename and the enclosing type is only trivial for java, but not for other languages. PHP and Javascript for example have no hard connection between filename and embedded type, so they can't be derived from each other.

For PHP for example the methodName is the combination of the type and the type. See here:

return new SingleMethod(fileObject, CommandUtils.encodeMethod(method.getPhpType().getFullyQualifiedName(), method.getName()));

and

public static String encodeMethod(String className, String methodName) {
return className + "::" + methodName; // NOI18N
}

@ratcashdev
Copy link
Contributor Author

ratcashdev commented May 4, 2022

As I see it, the only occasion, where the code is using the 3-arg constructor, is when executing a single test method, and this is not 'sent' or provided to other modules. All other cases (if any) will be using the 2-arg constructor, where there's no issue with the equals.
Besides, for a case like Dummy#test enclosing type will be null and therefore not breaking equals().
https://github.com/apache/netbeans/pull/3995/files#diff-fa5324f0386490b6cfc96b5331e831a50db1dc5703bc1d7b0f47636f22a43f1aR186

It will only be non-null, if the method is in a class like Dummy$EmbeddedClass#test, in which case enclosingType will be EmbeddedClass (assuming that the name of the file is Dummy.java), and then again, this is only done for JAVA/JUnit sources. So I don't have to care about PHP here.

@matthiasblaesing
Copy link
Contributor

The fact that the change only affects currently unsupported cases is a good argument, that this is a safe change.

I'm still uncomfortable with the fact, that the SingleMethod class changed, while the PHP module demonstrated a way to reference qualified names without changing the definition. In Javadoc there an established syntax to refer to members - a reference could be coded as package.class.innerclass#method(ParamType1,ParamType2).

An open question: is support for overloads required (implied with the parameter based syntax).

@ratcashdev
Copy link
Contributor Author

a reference could be coded as package.class.innerclass#method(ParamType1,ParamType2).

This is an encoded, not a structured form and therefore places more responsibility on the modules using it.

As it can be seen from the changes, both maven as well as gradle have their own way of treating embedded classes when executing test methods. If indeed we were providing an encoded form, modules would need to invent/write their own ways to parse the encoded form, tokenize them and pass it on to the engine to run it.
Since I provide the structured form, working with the data is much easier, no parsing/tokenization is necessary in the modules.

@matthiasblaesing
Copy link
Contributor

I'm not convinced. Anyway, if another commiter wants to merge this, I won't stand in the way.

What must be changed before a merge, is that the author information is fixed. This needs a real name in it.

@ratcashdev
Copy link
Contributor Author

What must be changed before a merge, is that the author information is fixed. This needs a real name in it.

Happy to sign a formal CLA.

@neilcsmith-net neilcsmith-net modified the milestones: NB15, NB16 Jul 18, 2022
public SingleMethod(FileObject file, String methodName, String enclosingType) {
super();
if (file == null) {
throw new IllegalArgumentException("file is <null>");
Copy link
Member

Choose a reason for hiding this comment

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

Consider to use org.openide.util.Parameters.notNull()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

just copied the existing code. Is NPE indeed better than the above? Or just a pure Objects.requireNotNull()...

this.methodName = methodName;
}

public SingleMethod(FileObject file, String methodName, String enclosingType) {
Copy link
Member

Choose a reason for hiding this comment

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

Document the syntax of the enclosing type: should it be FQN, or just fragment starting from the simple name of the toplevel class in the file ... ? Note that from the other usages, it seems that the enclosingType must start with $ character. While possible, it's not a nice API ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This class in just a placeholder, or container. There's no specific contract as of the format or even the usage of the enclosing type - hence no javadoc on it. I guess different languages will have different requirements.

You're right, however, that a kind of a convention is introduced by the TestClassInfoTask class in the JUnit Test UI, putting a leading $ into it (or null). Not sure if it's the right place to document a convention specific to a language (or just a test runner framework) in this classes constructor.

if (classOnly.startsWith(fileName) && classOnly.length() > fileName.length()) {
return classOnly.substring(fileName.length());
}
return null;
Copy link
Member

@sdedic sdedic Jul 19, 2022

Choose a reason for hiding this comment

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

Note: there may be multiple toplevel classes in a file: how is the SingleMethod supposed to capture 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.

Good question. How is it done today?

Element element = trees.getElement(trees.getPath(compilationUnitTree, tree));
if (element != null && element.getKind() == ElementKind.CLASS && element.getSimpleName().contentEquals(fo2open[0].getName())) {
List<? extends ExecutableElement> methodElements = ElementFilter.methodsIn(element.getEnclosedElements());
Element classElement = getClassElement(element, desiredClassName);
Copy link
Member

Choose a reason for hiding this comment

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

this could eventually return another ClassElement within the compilation unit with the same simple name, i.e. in a case

class Foo {
    class A {
       class C {}
    }
    class B {
       class C{} 
    }
}

the extractDeepestClass(B.C) seems to produce "C", and getClassElement("C") could find A.C instead of B.C ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Valid. will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fix pushed. I guess the challenge of navigating to a specific class/method is quite common and should be solved once. Right now, a code like this is written at least 3 times (Gradle, TestNg, Maven/JUnit) so it would be great to have it unified, but I think it would be best to leave such a change it to a pure 'refactoring PR'

// keep the embedded class in classnames and add to display name
int suiteNameLength = suite.getName().length();
if (classname.length() > suiteNameLength && classname.startsWith(suite.getName())) {
displayName = classname.substring(suiteNameLength) + "." + methodName;
Copy link
Member

Choose a reason for hiding this comment

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

Q: when is this code path executed ? I have tried running the whole testsuite, and a single method within a nested class, but did not hit the branch. I am asking bcs it seems as there should be '.' or '$' at suiteNameLength-th character, so '.' would be duplicated.

Copy link
Contributor Author

@ratcashdev ratcashdev Sep 1, 2022

Choose a reason for hiding this comment

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

Have you tried using the @Nested JUnit5 annotation?

class MyClassTest {

  @Nested
  class NestedGroupTest {
    @Test
    public void mytest() {...}
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it should look like this:
image

@ratcashdev ratcashdev requested review from sdedic and removed request for dbalek, entlicher and matthiasblaesing September 2, 2022 07:49
@neilcsmith-net
Copy link
Member

@sdedic @matthiasblaesing marking as stale and removing from NB16 milestone for now. Please take a look and re-add to milestone if you think it should be merged.

@neilcsmith-net neilcsmith-net added the stale No recent activity - likely to be closed. label Oct 11, 2022
@neilcsmith-net neilcsmith-net removed this from the NB16 milestone Oct 11, 2022
@ratcashdev
Copy link
Contributor Author

Is there anything I can do to stop this from being stale? I thought I have answered all question, or at least reacted.

@mbien mbien added Maven [ci] enable "build tools" tests ci:all-tests [ci] enable all tests ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) labels Sep 22, 2023
@mbien
Copy link
Member

mbien commented Sep 22, 2023

@ratcashdev would you like to fix the small conflict in the imports so that CI can do a fresh run?

@ratcashdev
Copy link
Contributor Author

@mbien will have a look.

@ratcashdev
Copy link
Contributor Author

@mbien pushed

@vsmid
Copy link

vsmid commented Oct 16, 2024

Hi, this one could be a really useful addition. Any news on this?

@ratcashdev
Copy link
Contributor Author

@mbien Realizing that this PR has been lying abandoned by the maintainers for 3 years, I feel uncertain to invest the time & energy to update it with regards to the conflicts (and alternative concept implemented) in SingleMethod.java.

If this functionality is something the maintainers do not want to have inside NetBeans, I accept it (and it will mean I will have to switch to IntelliJ). If the implementation is not something you like, feel free to suggest or come up with something better. I will also take no offense if anyone will say my implementation is a scratch-my-own itch. It may be so. After all, I have zero knowledge of the internals of NB and no confidence in refactoring major internals. Either way, either move this forward with some help & guidance, or be clear about the future of this functionality being dead.

@mbien
Copy link
Member

mbien commented Apr 2, 2025

@ratcashdev I haven't encountered the nested tests yet but it would certainly be nice to have support for it. Other than that I can't promise anything. I do this in my freetime and as time permits. Once per release I try to go through some community contributed PRs which look ready and try to get some in if possible. There are only so many side quests I can accept at a time and there is typically always something else showing up.

@matthiasblaesing
Copy link
Contributor

Does PR #7979 fix the nested class use-case completly or is there anything missing?

@matthiasblaesing
Copy link
Contributor

Answering my own question, I tested the SampleTest from #3995 and get incomplete/wrong rendering of the test, disfunctional "go to source" and missing "Run again" and "Debug" functionality:

grafik

@ratcashdev I raised concerns because you modified SingleMethod, that concern is not valid anymore as #7979 with a different description already made that change, so we should make the best from this.

@ratcashdev
Copy link
Contributor Author

@matthiasblaesing PR updated. Smoke test on a local build works nicely.

@mbien mbien removed the stale No recent activity - likely to be closed. label Jun 2, 2025
@matthiasblaesing
Copy link
Contributor

@ratcashdev lets simplify the history a bit please. A deep history with multiple merges from master makes it difficult so see what is really going on and if necessary to revert after merge if necessary.

When doing that, please ensure, that all commits hold your full real name and email address (former is missing, latter is present).

I will not have time to really look at this the next few days, but what I saw immediatly was the change in Classpath.java:

What is the idea here? As far as I can see this breaks the existing contract. findPath is called by findResource, if I'm not missing something obvious, people can create a resouce dummy$name. That file was locateable before and is not afterwards. This looks as if you want to delegate resolving Demo$Nested.class to Demo.class, but from my POV that is the wrong location as it has not context to judge whether that modification is fine or not.

@ratcashdev ratcashdev force-pushed the feature/support-nested-junit branch from a3a565d to b58c367 Compare June 3, 2025 06:39
@ratcashdev
Copy link
Contributor Author

@matthiasblaesing I have rebased the history, it's clean now. I understand your reasons for asking for my real name, but I also have my own reasons (and they are not malicious) for not doing so. Please try to accept it. I am OK signing a formal cooperation agreement, if that helps, subject to that not being public.

ad Classpath.java: The point was exactly to allow finding the Demo$Nested.class, as it was not working without that change. However, that was 3 years ago, maybe the situation is different now, worth a check. In any case, if it's the wrong place, where's the better place?

@matthiasblaesing
Copy link
Contributor

For the name problem: I read a bit and I think I would be ok with merging.

For the code updates - I don't think this works correctly yet. I created a maven project with our sample code (adjusted method names to better see the differences):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test</groupId>
    <artifactId>TestNested</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>17</maven.compiler.release>
        <exec.mainClass>test.testnested.TestNested</exec.mainClass>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.11.3</version>
            <scope>test</scope>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.11.3</version>
            <scope>test</scope>
            <type>jar</type>
        </dependency>
    </dependencies>
</project>
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class SampleTest {
  @Test
  public void testMyMethod1() {
    System.out.println("write this");
  }
  @Nested
  class NestedClass {
    @Test
    public void testMyMethod2() {
      System.out.println("nested write 2");
    }
  }
  @Nested
  class NestedClass2 {
    @Test
    public void testMyMethod3() {
      System.out.println("nested write 1");
    }
    @Test
    public void testMyMethod4() {
      System.out.println("nested write 3");
    }
    @Nested
    class DoubleNestedClass3 {
      @Test
      public void testMyMethod5() {
        System.out.println("double nested write 4");
      }
      @Test
      public void testmyMethod6() throws Exception {
        throw new Exception();
      }
    }
  }
}

When I run "Test" from the projects context menu I see:

grafik

cd /tmp/TestNested; JAVA_HOME=/home/matthias/bin/jdk-17 /home/matthias/src/netbeans/nbbuild/netbeans/java/maven/bin/mvn --no-transfer-progress test

What I would have expected:

  • testMyMethod1 should be shown under SampleTest
  • testMyMethod2 should be shown under SampleTest$NestedClass
  • testMyMethod3 should be shown under SampleTest$NestedClass2
  • testMyMethod4 should be shown under SampleTest$NestedClass2

When I choose "Test File" from the context menu in the editor window in SampleTest.java I get:

grafik

cd /tmp/TestNested; JAVA_HOME=/home/matthias/bin/jdk-17 /home/matthias/src/netbeans/nbbuild/netbeans/java/maven/bin/mvn -Dtest=SampleTest,SampleTest$* --no-transfer-progress process-test-classes surefire:test
  • testMyMethod1, testMyMethod3 and testMyMethod4 are not execute/analyzed

When I try to run a single method from the test file directly:

grafik

This fails for testMyMethod1 with an exception:

SEVERE [org.openide.util.RequestProcessor]: Error in RequestProcessor org.netbeans.modules.maven.ActionProviderImpl$$Lambda$1096/0x0000782694f2ec20
java.lang.NullPointerException: Cannot invoke "org.netbeans.spi.project.NestedClass.getClassName()" because the return value of "org.netbeans.spi.project.SingleMethod.getNestedClass()" is null
	at org.netbeans.modules.maven.execute.DefaultReplaceTokenProvider.createReplacements(DefaultReplaceTokenProvider.java:250)
	at org.netbeans.modules.maven.ActionProviderImpl.replacements(ActionProviderImpl.java:426)
	at org.netbeans.modules.maven.ActionProviderImpl.invokeAction(ActionProviderImpl.java:308)
	at org.netbeans.modules.maven.ActionProviderImpl.lambda$invokeAction$4(ActionProviderImpl.java:281)
	at org.openide.util.RequestProcessor$Task.run(RequestProcessor.java:1403)
	at org.netbeans.modules.openide.util.GlobalLookup.execute(GlobalLookup.java:45)
	at org.openide.util.lookup.Lookups.executeWith(Lookups.java:287)
	at org.openide.util.RequestProcessor$Processor.run(RequestProcessor.java:2018)

For testMyMethod2 it fails a bit different:

Invocation:

cd /tmp/TestNested; JAVA_HOME=/home/matthias/bin/jdk-17 /home/matthias/src/netbeans/nbbuild/netbeans/java/maven/bin/mvn -Dtest=SampleTestNestedClass#testMyMethod2 --no-transfer-progress process-test-classes surefire:test

Result:

Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test (default-cli) on project TestNested: No tests matching pattern "SampleTestNestedClass#testMyMethod2" were executed! (Set -Dsurefire.failIfNoSpecifiedTests=false to ignore this error.) -> [Help 1]

For the change to Classpath.java: As indicated breaking API contract is not acceptable. I wanted to have a look at what breaks when I remove the change, but the points above invalidated that quest.

@ratcashdev
Copy link
Contributor Author

ratcashdev commented Jun 5, 2025

@matthiasblaesing Something's off here. This is how it looks for me in a fresh build (ant tryme).
image

Executing individual tests fail though; The mvn command line argument is incorrect, is missing a $:
-Dtest=io.test.maven.beans.SampleTestNestedClass#testMyMethod2

@ratcashdev
Copy link
Contributor Author

Pushed a fix to execute individual tests ("Run focused test method") with the correct maven arguments.

However, "Run Again" and "Debug" does not work yet.
Right now Netbeans sends maven the argument -Dtest=io.test.maven.beans.SampleTest#io.test.maven.beans.SampleTest$NestedClass2.testMyMethod3 which is clearly wrong. I have zero clue where to fix this though. Help is appreciated.

@matthiasblaesing
Copy link
Contributor

The run single case works now. But the output problem is still there. Please ensure that you run the manuel test (tryme) from a clean build:

  • ensure there are no pending changes git reset --hard
  • ensure there are no uncommitted files present git clean -f -x -d
  • build again and run with ant tryme
  • check the title of the main window, the hash shown there must now match the final commit 8cf15222cefcf822adcff1735d94de6bf90eb29a

@ratcashdev
Copy link
Contributor Author

Did as suggested regarding the build procedure. The hash is as expected.
The screenshot for the "Test result" is exactly the same as I have attached above. Very different compared to what you posted yourself. I wonder what build were you running?

But the output problem is still there.

What exactly is the output problem? Please be more specific.

as for the "Run Again" topic:
I don't know how to do it, but I think the best would be to use multiple levels of nodes in the tree of the Test Results, because the Run Again feature uses the parent as classname and the leaf as method for maven run. Following this principle the Run Again could work properly without additional hacks. However, but I am not sure if this is something the rest of the codebase would be able to handle.

@matthiasblaesing
Copy link
Contributor

I experimented a bit more today and this a race condition. Running the test setup multiple times, I sometimes get:

grafik

and sometimes I get:

grafik

@matthiasblaesing
Copy link
Contributor

Closing this as #8664 was finally merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:all-tests [ci] enable all tests ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) Gradle [ci] enable "build tools" tests Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) Maven [ci] enable "build tools" tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants