diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 269b2dd..441bd57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,10 +9,12 @@ on: jobs: matrix-build: runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: true matrix: - java: [11] # 17 & 21 removed because of https://github.com/itsallcode/junit5-system-extensions/issues/68 + java: [11, 17, 21] concurrency: group: ${{ github.workflow }}-${{ github.ref }}-java-${{ matrix.java }} cancel-in-progress: true @@ -46,12 +48,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Publish Test Report - uses: scacap/action-surefire-report@v1 - if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} - with: - report_paths: '**/target/surefire-reports/TEST-*.xml' - github_token: ${{ secrets.GITHUB_TOKEN }} build: needs: matrix-build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 96d8ac2..6fd6438 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -10,6 +10,9 @@ jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + contents: read + security-events: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/github_release.sh b/.github/workflows/github_release.sh new file mode 100755 index 0000000..a4cc1a4 --- /dev/null +++ b/.github/workflows/github_release.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +base_dir="$( cd "$(dirname "$0")/../.." >/dev/null 2>&1 ; pwd -P )" +readonly base_dir +readonly pom_file="$base_dir/pom.xml" + +# Read project version from pom file +project_version=$(grep "" "$pom_file" | sed --regexp-extended 's/\s*(.*)<\/version>\s*/\1/g' | head --lines=1) +readonly project_version +echo "Read project version '$project_version' from $pom_file" + +readonly changes_file="$base_dir/doc/changes/changes_${project_version}.md" +notes=$(cat "$changes_file") +readonly notes + +readonly title="Release $project_version" +readonly tag="$project_version" +echo "Creating release:" +echo "Git tag : $tag" +echo "Title : $title" +echo "Changes file : $changes_file" + +release_url=$(gh release create --latest --title "$title" --notes "$notes" --target main "$tag") +readonly release_url +echo "Release URL: $release_url" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..11b551c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release + +on: + workflow_dispatch: + inputs: + skip-deploy-maven-central: + description: "Skip deployment to Maven Central" + required: true + type: boolean + default: false + +jobs: + release: + runs-on: ubuntu-latest + defaults: + run: + shell: "bash" + concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + permissions: + contents: write # Required for creating GitHub release + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Fail if not running on main branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: actions/github-script@v7 + with: + script: | + core.setFailed('Not running on main branch, github.ref is ${{ github.ref }}. Please start this workflow only on main') + + - name: Set up Maven Central Repository + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: 17 + cache: "maven" + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Build + run: mvn --batch-mode -T 1C clean install + + - name: List secret GPG keys + run: gpg --list-secret-keys + + - name: Publish to Maven Central Repository + if: ${{ !inputs.skip-deploy-maven-central }} + run: mvn --batch-mode deploy -Possrh -DstagingDescription="Deployed via GitHub workflow release.yml" + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} + + - name: Create GitHub Release + run: ./.github/workflows/github_release.sh + env: + GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 8b7feeb..cb6ddac 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,15 @@ The `ExitGuard` temporarily replaces the existing security manager. From version 1.2.0 on if a security guard existed before, it serves as a delegate for all security checks with the exception of the `checkExit`. +**Warning:** The JREs Security Manager used by `ExitGuard` is deprecated and is not supported by Java 21 and later. It still works with Java 17 but logs the following warning: + +``` +WARNING: A terminally deprecated method in java.lang.System has been called +WARNING: System::setSecurityManager has been called by ... +WARNING: Please consider reporting this to the maintainers of ... +WARNING: System::setSecurityManager will be removed in a future release +``` + ## Asserting Data Sent to `System.out` To capture data sent to `System.out`, follow these steps: @@ -136,40 +145,17 @@ mvn --update-snapshots versions:display-dependency-updates versions:display-plug ``` ### Publishing to MavenCentral - -1. Add the following to your `~/.m2/settings.xml`: - - ```xml - - - - ossrh - your-jira-id - your-jira-pwd - - - - - ossrh - - true - - - gpg - the_pass_phrase - - - - - ``` +#### Prepare the Release 1. Checkout the `main` branch. -1. Update version in `pom.xml`, commit and push. -1. Run command +2. Update version in `pom.xml` and changelog. +3. Commit and push changes. +4. Create a new pull request, have it reviewed and merged to `main`. - ```bash - mvn -DskipSigningArtifacts=false clean deploy - ``` +### Perform the Release -1. Create a [release](https://github.com/itsallcode/junit5-system-extensions/releases) of the `main` branch on GitHub. -1. After some time the release will be available at [Maven Central](https://repo1.maven.org/maven2/org/itsallcode/junit5-system-extensions/). +1. Start the release workflow + * Run command `gh workflow run release.yml --repo itsallcode/junit5-system-extensions --ref main` + * or go to [GitHub Actions](https://github.com/itsallcode/junit5-system-extensions/actions/workflows/release.yml) and start the `release.yml` workflow on branch `main`. +2. Update title and description of the newly created [GitHub release](https://github.com/itsallcode/junit5-system-extensions/releases). +3. After some time the release will be available at [Maven Central](https://repo1.maven.org/maven2/org/itsallcode/junit5-system-extensions/). diff --git a/doc/changes/changes_1.2.1.md b/doc/changes/changes_1.2.1.md index c72d8e5..90e9293 100644 --- a/doc/changes/changes_1.2.1.md +++ b/doc/changes/changes_1.2.1.md @@ -1,10 +1,10 @@ -# JUnit5 System Extensions 1.2.1, released 2022-09-?? +# JUnit5 System Extensions 1.2.1, released 2024-07-09 Code name: Sonar smell fixes for 1.2.0 ## Summary -Version 1.2.1 is a service release that fixed SONAR code smells. +Version 1.2.1 is a service release that fixed SONAR code smells and adds tests with Java 17 and Java 21. ## Bugfixes @@ -14,7 +14,12 @@ Version 1.2.1 is a service release that fixed SONAR code smells. * [PR #67](https://github.com/itsallcode/junit5-system-extensions/pull/66): Upgrade dependencies * [PR #69](https://github.com/itsallcode/junit5-system-extensions/pull/69): Remove license header from sources +* [PR #72](https://github.com/itsallcode/junit5-system-extensions/pull/72): Adapt to Java 17 and 21, add JavaDoc ## Development process * The `develop` branch was renamed to `main`, `master` was deleted. + +## Deprecation Warning + +The JREs Security Manager used by `ExitGuard` is deprecated and is not supported by Java 21 and later. diff --git a/pom.xml b/pom.xml index 3113c1c..fa3ab1e 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 11 - 5.10.1 + 5.10.3 UTF-8 true https://sonarcloud.io @@ -67,7 +67,7 @@ org.mockito mockito-core - 5.10.0 + 5.12.0 test @@ -76,7 +76,7 @@ org.apache.maven.plugins maven-toolchains-plugin - 3.1.0 + 3.2.0 @@ -95,16 +95,21 @@ org.apache.maven.plugins maven-compiler-plugin - 3.12.1 + 3.13.0 ${java.version} ${java.version} + + + -Xlint:all,-removal + -Werror + org.apache.maven.plugins maven-source-plugin - 3.3.0 + 3.3.1 attach-sources @@ -117,7 +122,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.3 + 3.7.0 attach-javadocs @@ -141,7 +146,7 @@ org.jacoco jacoco-maven-plugin - 0.8.11 + 0.8.12 @@ -158,13 +163,18 @@ + org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.3.0 + + + -XX:+EnableDynamicAgentLoading ${argLine} + org.apache.maven.plugins maven-gpg-plugin - 3.1.0 + 3.2.4 sign-artifacts @@ -181,12 +191,12 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.1 + 3.1.2 org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.7.0 true ossrh @@ -197,7 +207,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.4.1 + 3.5.0 enforce-maven diff --git a/src/main/java/org/itsallcode/io/Capturable.java b/src/main/java/org/itsallcode/io/Capturable.java index 32e1afb..1fccd59 100644 --- a/src/main/java/org/itsallcode/io/Capturable.java +++ b/src/main/java/org/itsallcode/io/Capturable.java @@ -1,22 +1,25 @@ package org.itsallcode.io; +/** + * Interface for classes that can capture output. + */ public interface Capturable { /** * Activate capturing */ - public void capture(); + void capture(); /** * Activate muted capturing, i.e. don't forward output to the underlying * output stream. This can be useful to speedup tests. */ - public void captureMuted(); + void captureMuted(); /** * Get the data that was captured. * * @return captured data. */ - public String getCapturedData(); + String getCapturedData(); } diff --git a/src/main/java/org/itsallcode/io/CapturingOutputStream.java b/src/main/java/org/itsallcode/io/CapturingOutputStream.java index 0fc5fa3..f9cd728 100644 --- a/src/main/java/org/itsallcode/io/CapturingOutputStream.java +++ b/src/main/java/org/itsallcode/io/CapturingOutputStream.java @@ -1,17 +1,26 @@ package org.itsallcode.io; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.Objects; +/** + * This {@link OutputStream} captures the output written to it and makes it + * available as a string. + */ public class CapturingOutputStream extends OutputStream implements Capturable { - private OutputStream targetStream = null; - private ByteArrayOutputStream internalStream = null; - private String captureBuffer = null; + private OutputStream targetStream; + private ByteArrayOutputStream internalStream; + private String captureBuffer; private boolean forwardOutputToTarget = true; + /** + * Creates a new instance of this class. + * + * @param targetStream + * the stream to which the output should be forwarded. + */ public CapturingOutputStream(final OutputStream targetStream) { this.targetStream = Objects.requireNonNull(targetStream, "targetStream"); @@ -61,7 +70,7 @@ public synchronized void close() throws IOException { if (this.internalStream != null) { - this.captureBuffer = this.internalStream.toString(); + this.captureBuffer = this.internalStream.toString(StandardCharsets.UTF_8); this.internalStream.close(); } this.internalStream = null; @@ -93,7 +102,7 @@ public String getCapturedData() } else { - return this.internalStream.toString(); + return this.internalStream.toString(StandardCharsets.UTF_8); } } diff --git a/src/main/java/org/itsallcode/junit/sysextensions/AbstractSystemOutputGuard.java b/src/main/java/org/itsallcode/junit/sysextensions/AbstractSystemOutputGuard.java index 30e3c53..5dbf1fe 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/AbstractSystemOutputGuard.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/AbstractSystemOutputGuard.java @@ -1,28 +1,37 @@ package org.itsallcode.junit.sysextensions; -import java.io.IOException; -import java.io.PrintStream; +import java.io.*; import java.lang.annotation.Annotation; import java.lang.reflect.Executable; import java.lang.reflect.Parameter; +import java.nio.charset.StandardCharsets; import org.itsallcode.io.Capturable; import org.itsallcode.io.CapturingOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.*; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolutionException; -import org.junit.jupiter.api.extension.ParameterResolver; +/** + * Base class for JUnit 5 extensions that guard {@link System#out} and + * {@link System#err} streams. + */ public abstract class AbstractSystemOutputGuard implements BeforeEachCallback, ParameterResolver, AfterEachCallback { + /** The key for the previous output stream. */ protected static final String PREVIOUS_OUTPUT_STREAM_KEY = "PREV_OSTREAM"; + /** The key for the capturing output stream. */ protected static final String CAPTURING_OUTPUT_STREAM_KEY = "CAPT_OSTREAM"; + /** + * Creates a new instance of this class. + */ + protected AbstractSystemOutputGuard() + { + // Default constructor + } + @Override public void beforeEach(final ExtensionContext context) throws Exception { @@ -36,8 +45,18 @@ private void saveCurrentSystemStream(final ExtensionContext context) context.getStore(getNamespace()).put(PREVIOUS_OUTPUT_STREAM_KEY, getSystemStream()); } + /** + * Get the namespace for the extension. + * + * @return the namespace + */ protected abstract Namespace getNamespace(); + /** + * Get the system stream. + * + * @return the system stream + */ protected abstract PrintStream getSystemStream(); private void flushSystemStream() @@ -49,12 +68,26 @@ private void replaceSystemStreamWithCapturingStream(final ExtensionContext conte { final CapturingOutputStream capturingStream = new CapturingOutputStream(getSystemStream()); context.getStore(getNamespace()).put(CAPTURING_OUTPUT_STREAM_KEY, capturingStream); - final PrintStream printStream = new PrintStream(getCapturingOutputStream(context)); + final PrintStream printStream = new PrintStream(getCapturingOutputStream(context), false, + StandardCharsets.UTF_8); setSystemStream(printStream); } + /** + * Set the system stream to the given stream. + * + * @param systemStream + * the stream to set + */ protected abstract void setSystemStream(final PrintStream systemStream); + /** + * Get the capturing output stream from the context. + * + * @param context + * the context + * @return the capturing output stream + */ protected CapturingOutputStream getCapturingOutputStream(final ExtensionContext context) { return (CapturingOutputStream) context.getStore(getNamespace()).get(CAPTURING_OUTPUT_STREAM_KEY); @@ -68,12 +101,26 @@ public boolean supportsParameter(final ParameterContext parameterContext, final && isCompatibleAnnotation(parameterContext); } + /** + * Check if the method is viable for parameter resolution. + * + * @param parameterContext + * the parameter context + * @return {@code true} if the method is viable + */ protected boolean isViableMethod(final ParameterContext parameterContext) { final Executable method = parameterContext.getDeclaringExecutable(); return method.isAnnotationPresent(Test.class) || method.isAnnotationPresent(BeforeEach.class); } + /** + * Check if the parameter is a capturing stream. + * + * @param parameterContext + * the parameter context + * @return {@code true} if the parameter is a capturing stream + */ protected boolean isParameterCapturingStream(final ParameterContext parameterContext) { return (parameterContext.getParameter().getType().equals(Capturable.class)); @@ -86,6 +133,11 @@ private boolean isCompatibleAnnotation(final ParameterContext parameterContext) } + /** + * Get the parameter annotation that indicates the parameter is a capturing. + * + * @return the parameter annotation + */ protected abstract Class getParameterAnnotation(); @Override @@ -107,13 +159,33 @@ private void restorePreviousSystemStream(final ExtensionContext context) setSystemStream(getPreviousStream(context)); } + /** + * Get the previous stream from the context. + * + * @param context + * the context + * @return the previous stream + */ protected PrintStream getPreviousStream(final ExtensionContext context) { return (PrintStream) context.getStore(getNamespace()).get(PREVIOUS_OUTPUT_STREAM_KEY); } - protected void closeCapturingOutputStream(final ExtensionContext context) throws IOException + /** + * Close the capturing output stream. + * + * @param context + * the context + */ + protected void closeCapturingOutputStream(final ExtensionContext context) { - getCapturingOutputStream(context).close(); + try + { + getCapturingOutputStream(context).close(); + } + catch (final IOException exception) + { + throw new UncheckedIOException("Failed to close capturing output stream", exception); + } } } diff --git a/src/main/java/org/itsallcode/junit/sysextensions/AssertExit.java b/src/main/java/org/itsallcode/junit/sysextensions/AssertExit.java index 129ff94..1f38d01 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/AssertExit.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/AssertExit.java @@ -3,10 +3,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +import java.util.logging.Logger; + import org.itsallcode.junit.sysextensions.security.ExitTrapException; +/** + * Utility class to assert that a lambda expression calls + * {@link System#exit(int)}. + */ public final class AssertExit { + private static final Logger LOGGER = Logger.getLogger(AssertExit.class.getName()); private AssertExit() { // prevent instantiation @@ -25,8 +32,10 @@ public static void assertExit(final Runnable runnable) { runnable.run(); } - catch (final ExitTrapException e) + catch (final ExitTrapException exception) { + LOGGER.fine( + () -> "Caught ExitTrapException " + exception + " with exit status " + exception.getExitStatus()); return; } failMissingExit(); @@ -53,9 +62,11 @@ public static void assertExitWithStatus(final int expectedExitCode, final Runnab { runnable.run(); } - catch (final ExitTrapException e) + catch (final ExitTrapException exception) { - assertEquals(expectedExitCode, e.getExitStatus(), "Expected exit status code"); + LOGGER.fine( + () -> "Caught ExitTrapException " + exception + " with exit status " + exception.getExitStatus()); + assertEquals(expectedExitCode, exception.getExitStatus(), "Expected exit status code"); return; } failMissingExit(); diff --git a/src/main/java/org/itsallcode/junit/sysextensions/ExitGuard.java b/src/main/java/org/itsallcode/junit/sysextensions/ExitGuard.java index 5546838..47025e4 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/ExitGuard.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/ExitGuard.java @@ -1,19 +1,30 @@ package org.itsallcode.junit.sysextensions; import org.itsallcode.junit.sysextensions.security.ExitGuardSecurityManager; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; -import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; -import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.*; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.TestInstancePostProcessor; +/** + * JUnit 5 extension that guards calls to {@link System#exit(int)}. + * + * @deprecated Class {@link SecurityManager} used by ExitGuard is deprecated and + * does not work with JRE 21 and later. + */ +@Deprecated(since = "1.2.1", forRemoval = true) public final class ExitGuard implements TestInstancePostProcessor, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterAllCallback { private static final String PREVIOUS_SECURITY_MANAGER_KEY = "PREV_SECMAN"; private static final String EXIT_GUARD_SECURITY_MANAGER_KEY = "EXIT_SECMAN"; + /** + * Creates a new instance of this class. + */ + public ExitGuard() + { + // Default constructor + } + @Override public void postProcessTestInstance(final Object testInstance, final ExtensionContext context) { @@ -21,12 +32,12 @@ public void postProcessTestInstance(final Object testInstance, final ExtensionCo installExitGuardSecurityManager(context); } - private void saveCurrentSecurityManager(final ExtensionContext context) + private static void saveCurrentSecurityManager(final ExtensionContext context) { context.getStore(getNamespace()).put(PREVIOUS_SECURITY_MANAGER_KEY, System.getSecurityManager()); } - private void installExitGuardSecurityManager(final ExtensionContext context) + private static void installExitGuardSecurityManager(final ExtensionContext context) { final SecurityManager previousSecurityManager = getPreviousSecurityManager(context); final SecurityManager exitGuardSecurityManager = new ExitGuardSecurityManager(previousSecurityManager); @@ -34,12 +45,12 @@ private void installExitGuardSecurityManager(final ExtensionContext context) context.getStore(getNamespace()).put(EXIT_GUARD_SECURITY_MANAGER_KEY, exitGuardSecurityManager); } - private Namespace getNamespace() + private static Namespace getNamespace() { return Namespace.create(ExitGuard.class); } - private ExitGuardSecurityManager getExitGuardSecurityManager(final ExtensionContext context) + private static ExitGuardSecurityManager getExitGuardSecurityManager(final ExtensionContext context) { return (ExitGuardSecurityManager) context.getStore(getNamespace()).get(EXIT_GUARD_SECURITY_MANAGER_KEY); } @@ -63,9 +74,9 @@ public void afterAll(final ExtensionContext context) throws Exception System.setSecurityManager(previousManager); } - private SecurityManager getPreviousSecurityManager(final ExtensionContext context) + private static SecurityManager getPreviousSecurityManager(final ExtensionContext context) { return (SecurityManager) context.getStore(getNamespace()) .get(PREVIOUS_SECURITY_MANAGER_KEY); } -} \ No newline at end of file +} diff --git a/src/main/java/org/itsallcode/junit/sysextensions/SystemErrGuard.java b/src/main/java/org/itsallcode/junit/sysextensions/SystemErrGuard.java index 40fda43..f1ddaed 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/SystemErrGuard.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/SystemErrGuard.java @@ -1,22 +1,30 @@ package org.itsallcode.junit.sysextensions; import java.io.PrintStream; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; import org.itsallcode.io.Capturable; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +/** + * JUnit 5 extension that guards {@link System#err} stream. + */ public class SystemErrGuard extends AbstractSystemOutputGuard { private static final Namespace NAMESPACE = Namespace.create(SystemErrGuard.class); + /** + * Creates a new instance of this class. + */ + public SystemErrGuard() + { + // Default constructor + } + /** * This annotation can be used on a parameter of type {@link Capturable} to - * ensure that the placeholder for System.err is injected. + * ensure that the placeholder for {@link System#out} is + * injected. */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/org/itsallcode/junit/sysextensions/SystemOutGuard.java b/src/main/java/org/itsallcode/junit/sysextensions/SystemOutGuard.java index cc5f63a..55a472b 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/SystemOutGuard.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/SystemOutGuard.java @@ -1,22 +1,29 @@ package org.itsallcode.junit.sysextensions; import java.io.PrintStream; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; import org.itsallcode.io.Capturable; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +/** + * JUnit 5 extension that guards {@link System#out} stream. + */ public class SystemOutGuard extends AbstractSystemOutputGuard { private static final Namespace NAMESPACE = Namespace.create(SystemOutGuard.class); + /** + * Creates a new instance of this class. + */ + public SystemOutGuard() + { + // Default constructor + } + /** * This annotation can be used on a parameter of type {@link Capturable} to - * ensure that the placeholder for System.out is injected. + * ensure that the placeholder for {@link System#out} is injected. */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/org/itsallcode/junit/sysextensions/security/ExitGuardSecurityManager.java b/src/main/java/org/itsallcode/junit/sysextensions/security/ExitGuardSecurityManager.java index 0d62855..97306f4 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/security/ExitGuardSecurityManager.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/security/ExitGuardSecurityManager.java @@ -4,11 +4,21 @@ import java.net.InetAddress; import java.security.Permission; +/** + * This security manager traps calls to {@link System#exit(int)} and throws an + * {@link ExitTrapException} instead. + */ public class ExitGuardSecurityManager extends SecurityManager { - private boolean trapExit = false; + private boolean trapExit; private final SecurityManager delegate; + /** + * Creates a new instance of this class. + * + * @param delegate + * the delegate security manager + */ public ExitGuardSecurityManager(final SecurityManager delegate) { this.delegate = delegate; @@ -263,4 +273,4 @@ public void checkWrite(final String file) this.delegate.checkWrite(file); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/itsallcode/junit/sysextensions/security/ExitTrapException.java b/src/main/java/org/itsallcode/junit/sysextensions/security/ExitTrapException.java index 4b30bbd..4128390 100644 --- a/src/main/java/org/itsallcode/junit/sysextensions/security/ExitTrapException.java +++ b/src/main/java/org/itsallcode/junit/sysextensions/security/ExitTrapException.java @@ -1,17 +1,41 @@ package org.itsallcode.junit.sysextensions.security; +/** + * This exception is thrown by {@link ExitGuardSecurityManager#checkExit(int)} + * when a call to {@link System#exit(int)} is intercepted. + */ public class ExitTrapException extends SecurityException { + @SuppressWarnings("java:S4926") // serialVersionUID used intentionally private static final long serialVersionUID = 3483205912039194022L; - /** @serial */ + + /** + * Exit status code. + * + * @serial + */ private final int status; + /** + * Constructs a new {@link ExitTrapException} with the given message and + * exit. + * + * @param message + * exit message + * @param status + * exit status code + */ public ExitTrapException(final String message, final int status) { super(message); this.status = status; } + /** + * Returns the exit status code. + * + * @return the exit status code + */ public int getExitStatus() { return this.status; diff --git a/src/test/java/org/itsallcode/io/TestCapturingOuputStream.java b/src/test/java/org/itsallcode/io/TestCapturingOutputStream.java similarity index 90% rename from src/test/java/org/itsallcode/io/TestCapturingOuputStream.java rename to src/test/java/org/itsallcode/io/TestCapturingOutputStream.java index 8c2d323..e1f8d4c 100644 --- a/src/test/java/org/itsallcode/io/TestCapturingOuputStream.java +++ b/src/test/java/org/itsallcode/io/TestCapturingOutputStream.java @@ -1,19 +1,14 @@ package org.itsallcode.io; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.*; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; +import java.io.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class TestCapturingOuputStream +class TestCapturingOutputStream { private static final byte[] CONTENT = "content".getBytes(); private OutputStream delegate; diff --git a/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExit.java b/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExit.java index 25106e7..bccfa32 100644 --- a/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExit.java +++ b/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExit.java @@ -7,8 +7,12 @@ import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; +@DisabledOnJre(value = { + JRE.JAVA_21 }, disabledReason = "The Security Manager is deprecated and will be removed in a future release") @ExtendWith(ExitGuard.class) class TestSystemExit { @@ -76,4 +80,4 @@ void testSystemWithStatusMissingExitThrowsAssertError() } fail("Code sequence where exit was expected did not exit, no assertion error was raised."); } -} \ No newline at end of file +} diff --git a/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExitReplacingExistingSecurityManager.java b/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExitReplacingExistingSecurityManager.java index ccc4d97..98a7726 100644 --- a/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExitReplacingExistingSecurityManager.java +++ b/src/test/java/org/itsallcode/junit/sysextensions/TestSystemExitReplacingExistingSecurityManager.java @@ -1,51 +1,52 @@ package org.itsallcode.junit.sysextensions; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import org.itsallcode.junit.sysextensions.security.ExitGuardSecurityManager; import org.itsallcode.junit.sysextensions.security.ExitTrapException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.JRE; +@DisabledOnJre(value = { + JRE.JAVA_21 }, disabledReason = "The Security Manager is deprecated and will be removed in a future release") class TestSystemExitReplacingExistingSecurityManager { @Test void testFirstSystemExitIntercepted() { - final SecurityManager previousSecuritymanager = System.getSecurityManager(); + final SecurityManager previousSecurityManager = System.getSecurityManager(); try { final SecurityManagerStub securityManagerStub = new SecurityManagerStub(); - final ExitGuardSecurityManager exitGuardsecurityManager = new ExitGuardSecurityManager(securityManagerStub); - System.setSecurityManager(exitGuardsecurityManager); - exitGuardsecurityManager.trapExit(true); + final ExitGuardSecurityManager exitGuardSecurityManager = new ExitGuardSecurityManager(securityManagerStub); + System.setSecurityManager(exitGuardSecurityManager); + exitGuardSecurityManager.trapExit(true); assertAll(() -> assertThrows(ExitTrapException.class, () -> System.exit(1)), () -> assertFalse(securityManagerStub.wasCheckExitCalled(), "Delegate exit called")); } finally { - System.setSecurityManager(previousSecuritymanager); + System.setSecurityManager(previousSecurityManager); } } @Test void testSecurityCheckGetsDelegated() { - final SecurityManager previousSecuritymanager = System.getSecurityManager(); + final SecurityManager previousSecurityManager = System.getSecurityManager(); try { final SecurityManagerStub securityManagerStub = new SecurityManagerStub(); - final ExitGuardSecurityManager exitGuardsecurityManager = new ExitGuardSecurityManager(securityManagerStub); - System.setSecurityManager(exitGuardsecurityManager); + final ExitGuardSecurityManager exitGuardSecurityManager = new ExitGuardSecurityManager(securityManagerStub); + System.setSecurityManager(exitGuardSecurityManager); System.getProperty("any-property"); assertTrue(securityManagerStub.wasCheckPropertyAccessCalled()); } finally { - System.setSecurityManager(previousSecuritymanager); + System.setSecurityManager(previousSecurityManager); } } @@ -77,4 +78,4 @@ public boolean wasCheckPropertyAccessCalled() return this.checkPropertyAccessCalled; } } -} \ No newline at end of file +} diff --git a/src/test/java/org/itsallcode/junit/sysextensions/TestSytemOutWithBuffer.java b/src/test/java/org/itsallcode/junit/sysextensions/TestSystemOutWithBuffer.java similarity index 85% rename from src/test/java/org/itsallcode/junit/sysextensions/TestSytemOutWithBuffer.java rename to src/test/java/org/itsallcode/junit/sysextensions/TestSystemOutWithBuffer.java index 6dae24c..49abe53 100644 --- a/src/test/java/org/itsallcode/junit/sysextensions/TestSytemOutWithBuffer.java +++ b/src/test/java/org/itsallcode/junit/sysextensions/TestSystemOutWithBuffer.java @@ -6,18 +6,12 @@ import java.io.PrintStream; import org.itsallcode.io.Capturable; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SystemOutGuard.class) -class TestSytemOutWithBuffer +class TestSystemOutWithBuffer { - @BeforeEach - void beforeEach() - { - } - @SuppressWarnings("squid:S2699") @Test void testGetCapturedRegular(final Capturable stream) throws IOException @@ -27,7 +21,7 @@ void testGetCapturedRegular(final Capturable stream) throws IOException } @Test - void testGetCapturedDataWithPrintStreamAroundSystemOut(final Capturable stream) throws IOException + void testGetCapturedDataWithPrintStreamAroundSystemOut(final Capturable stream) { final PrintStream printStream = new PrintStream(System.out); stream.capture();