averbraeck / djutils

Delft Java Utilities for statistics, stochastics, data analysis and serialization
BSD 3-Clause "New" or "Revised" License
0 stars 0 forks source link

Find new way to test terminating program without `setSecurityManager` in Java-17 #51

Closed averbraeck closed 1 hour ago

averbraeck commented 2 hours ago

In Java-17, the System.setSecurityManager function is deprecated and can disappear any moment. The error is:

WARNING: A terminally deprecated method in java.lang.System has been called
WARNING: System::setSecurityManager has been called by 
  com.github.stefanbirkner.systemlambda.SystemLambda 
  (file:/C:/Users/x/.m2/repository/com/github/stefanbirkner/system-lambda/1.2.1/system-lambda-1.2.1.jar)
WARNING: Please consider reporting this to the maintainers of 
  com.github.stefanbirkner.systemlambda.SystemLambda
WARNING: System::setSecurityManager will be removed in a future release

The method is used to test those CLI functions that calls System.exit(...) and of course should not exit the unit tests, since it is expected behavior.

averbraeck commented 2 hours ago

See https://openjdk.org/jeps/411 for the rationale. See https://openjdk.org/jeps/486 for the final removal in JDK-24.

In https://openjdk.org/jeps/486#Appendix an alternative is mentioned that uses a (dirty) trick to filter out the System.exit(...) call from the bytecode in the .class file when it is loaded. An implementation of this principle is already available in https://github.com/tginsberg/junit5-system-exit and this code works with Java-17. Another example is the JMockit library as mentioned in https://www.baeldung.com/junit-system-exit#1-dependency. Both install an instrumentation agent that changes the bytecode on the fly when it is being loaded.

averbraeck commented 2 hours ago

A first try with JMockit to see if we can make this work in Java 17.

In djutils-cli, we add the following dependency:

    <dependency>
      <groupId>org.jmockit</groupId>
      <artifactId>jmockit</artifactId>
      <version>1.49</version>
      <scope>test</scope>
    </dependency>

And the following plugin with instrumentation setting:

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${maven.surefire.version}</version>
        <configuration>
          <argLine>
            -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.49/jmockit-1.49.jar
          </argLine>
        </configuration>
      </plugin>
    </plugins>
  </build>

This should ensure that jmockit is loaded before junit starts, and that the coverage tests of jacoco are not influenced.

averbraeck commented 1 hour ago

In TestCliCommandLine, the test method testCli causes and exit because a port number is provided that is not between 1 and 65535.

The method not starts by mocking the System.exit(...) method:

        new MockUp<System>()
        {
            @Mock
            public void exit(final int value)
            {
                throw new RuntimeException(String.valueOf(value));
            }
        };

For the exit, it causes a RunTimeException instead.

In our code, we need to test if the correct error message is displayed via System.err, so we capture System.err to see if the correct error message is displayed and also check the exit of the program. Afterward, the System.err is reset to its original value (the console):

        PrintStream oldPrintStream = System.err;
        ByteArrayOutputStream errContent = new ByteArrayOutputStream();
        System.setErr(new PrintStream(errContent));
        try
        {
            CliUtil.execute(cmdErr, argsErr);
            fail("calling CliUtil.execute did not exit when it should");
        }
        catch (RuntimeException e)
        {
            assertTrue(errContent.toString().startsWith("Port should be between 1 and 65535"));
        }
        finally
        {
            System.setErr(oldPrintStream);
        }
averbraeck commented 1 hour ago

The code works correctly and code coverage also still works.

averbraeck commented 1 hour ago

Tests complete correctly in Maven. Note that a test with Run as... JUnit Test might not work anymore because the instrumentation will not be properly initialized.

averbraeck commented 1 hour ago

If you want to run in Eclipse, the -javaagent path should be explicitly provided. Unfortunately, this means that this is a manual addition to the run of the JUnit test:

image

So, in my case, the arguments are:

-ea -javaagent:C:/Users/alexandv/.m2/repository/org/jmockit/jmockit/1.49/jmockit-1.49.jar
averbraeck commented 1 hour ago

All unit tests run again, and are compliant with Java17+.