Skip to content

Instantly share code, notes, and snippets.

@syhily
Last active July 26, 2022 04:07
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save syhily/1ae31ba0394668eb040917387a698449 to your computer and use it in GitHub Desktop.
Save syhily/1ae31ba0394668eb040917387a698449 to your computer and use it in GitHub Desktop.
import org.junit.contrib.java.lang.system.internal.CheckExitCalled;
import org.junit.contrib.java.lang.system.internal.NoExitSecurityManager;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.platform.commons.support.ReflectionSupport;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.System.getSecurityManager;
import static java.lang.System.setSecurityManager;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/**
* This class is original inspired from project {@code system-rules}, rewrite it to adapt JUnit5.
* It is a JUnit Jupiter extension allows in-test specification of expected {@code System.exit(...)} calls.
*
* @author せいうはん (Employee ID: 17092068)
* @version 1.0.0, 2018-05-27 00:08
* @link https://github.com/stefanbirkner/system-rules/blob/master/src/main/java/org/junit/contrib/java/lang/system/ExpectedSystemExit.java
* @since 1.0.0, 2018-05-27 00:08
*/
@Target({TYPE, METHOD})
@Retention(RUNTIME)
@ExtendWith(ExpectedSystemExit.Extension.class)
public @interface ExpectedSystemExit {
class Extension implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, ParameterResolver, TestExecutionExceptionHandler {
private NoExitSecurityManager noExitSecurityManager;
private SecurityManager originalManager;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
noExitSecurityManager = new NoExitSecurityManager(getSecurityManager());
}
@Override
public void beforeEach(ExtensionContext context) {
originalManager = getSecurityManager();
setSecurityManager(noExitSecurityManager);
}
@Override
public void afterEach(ExtensionContext context) {
NoExitSecurityManager securityManager = (NoExitSecurityManager) getSecurityManager();
ExitCapture exitCapture = getExitCapture(context);
if (exitCapture.expectExit) {
checkSystemExit(securityManager, exitCapture);
}
setSecurityManager(originalManager);
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
boolean isTestMethodLevel = extensionContext.getTestMethod().isPresent();
boolean isOutputCapture = parameterContext.getParameter().getType() == ExitCapture.class;
return isTestMethodLevel && isOutputCapture;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return getExitCapture(extensionContext);
}
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
ExitCapture exitCapture = getExitCapture(context);
if (!(throwable instanceof CheckExitCalled) || !exitCapture.expectExit) {
throw throwable;
}
}
private void checkSystemExit(NoExitSecurityManager securityManager, ExitCapture exitCapture) {
if (securityManager.isCheckExitCalled()) {
exitCapture.handleSystemExitWithStatus(securityManager.getStatusOfFirstCheckExitCall());
} else {
exitCapture.handleMissingSystemExit();
}
}
private ExitCapture getExitCapture(ExtensionContext context) {
Store store = context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
return store.getOrComputeIfAbsent(ExitCapture.class, ReflectionSupport::newInstance, ExitCapture.class);
}
}
class ExitCapture {
private boolean expectExit = false;
private Integer expectedStatus = null;
public void expectSystemExit() {
expectExit = true;
}
public void expectSystemExitWithStatus(int status) {
expectSystemExit();
expectedStatus = status;
}
private void handleMissingSystemExit() {
if (expectExit) {
fail("System.exit has not been called.");
}
}
private void handleSystemExitWithStatus(int status) {
if (!expectExit) {
fail("Unexpected call of System.exit(" + status + ").");
} else if (expectedStatus != null) {
assertEquals(expectedStatus, Integer.valueOf(status), "Wrong exit status");
}
}
}
}
@rmcdouga
Copy link

I don't have much experience with developing annotations so I am struggling a bit with how to use this annotation (I can see that I can annotate my test class or my test method with this annotation, but I'm not seeing how I set the expected exit status value). Would it be possible to see an example of how this annotation is used i a JUnit 5 test?

@franzwong
Copy link

Example of how to use it.

@ExtendWith(ExpectedSystemExit.Extension.class)
class FooTest {
    @Test
    public void test(ExpectedSystemExit.ExitCapture exitCapture) {
        exitCapture.expectSystemExitWithStatus(1);
        System.exit(1);
    }
}

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