From 1f9b62b1228d1a251d0b61ef5cc8eb5d7de595d0 Mon Sep 17 00:00:00 2001 From: facewise Date: Fri, 5 Apr 2024 12:10:39 +0900 Subject: [PATCH] Fix broken AnsiOutput.detectIfAnsiCapable on JDK22 See gh-40172 --- settings.gradle | 1 + .../springframework/boot/ansi/AnsiOutput.java | 20 ++- .../spring-boot-console-tests/build.gradle | 44 ++++++ .../build.gradle | 17 +++ .../settings.gradle | 15 +++ .../consoleapp/ConsoleTestApplication.java | 32 +++++ .../boot/console/ConsoleIntegrationTests.java | 125 ++++++++++++++++++ .../src/intTest/resources/logback.xml | 4 + 8 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/src/main/java/org/springframework/boot/consoleapp/ConsoleTestApplication.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/java/org/springframework/boot/console/ConsoleIntegrationTests.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index 54a7bc343c7..0421ca35a27 100644 --- a/settings.gradle +++ b/settings.gradle @@ -75,6 +75,7 @@ include "spring-boot-project:spring-boot-test" include "spring-boot-project:spring-boot-testcontainers" include "spring-boot-project:spring-boot-test-autoconfigure" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configuration-processor-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-console-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-classic-tests" diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index da186faac6d..70511bde3fe 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,19 @@ package org.springframework.boot.ansi; +import java.io.Console; +import java.lang.reflect.Method; import java.util.Locale; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Generates ANSI encoded output, automatically attempting to detect if the terminal * supports ANSI. * * @author Phillip Webb + * @author Yong-Hyun Kim * @since 1.0.0 */ public abstract class AnsiOutput { @@ -152,8 +156,18 @@ public abstract class AnsiOutput { if (Boolean.FALSE.equals(consoleAvailable)) { return false; } - if ((consoleAvailable == null) && (System.console() == null)) { - return false; + if (consoleAvailable == null) { + Console c = System.console(); + if (c == null) { + return false; + } + Method isTerminalMethod = ClassUtils.getMethodIfAvailable(Console.class, "isTerminal"); + if (isTerminalMethod != null) { + Boolean isTerminal = (Boolean) isTerminalMethod.invoke(c); + if (Boolean.FALSE.equals(isTerminal)) { + return false; + } + } } return !(OPERATING_SYSTEM_NAME.contains("win")); } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/build.gradle new file mode 100644 index 00000000000..3b90a0ee07a --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/build.gradle @@ -0,0 +1,44 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" + id "org.springframework.boot.integration-test" +} + +description = "Spring Boot Console Integration Tests" + +configurations { + app +} + +dependencies { + app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter", configuration: "mavenRepository") + + intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + intTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + intTestImplementation("org.testcontainers:junit-jupiter") + intTestImplementation("org.testcontainers:testcontainers") +} + +task syncMavenRepository(type: Sync) { + from configurations.app + into "${buildDir}/int-test-maven-repository" +} + +task syncAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-console-tests-app") + destinationDirectory = file("${buildDir}/spring-boot-console-tests-app") +} + +task buildApp(type: GradleBuild) { + dependsOn syncAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-console-tests-app" + startParameter.buildCacheEnabled = false + tasks = ["build"] +} + +intTest { + dependsOn buildApp +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/build.gradle new file mode 100644 index 00000000000..6447e866030 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "java" + id "org.springframework.boot" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/settings.gradle new file mode 100644 index 00000000000..06d9554ad0d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/src/main/java/org/springframework/boot/consoleapp/ConsoleTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/src/main/java/org/springframework/boot/consoleapp/ConsoleTestApplication.java new file mode 100644 index 00000000000..8ed6d209244 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/spring-boot-console-tests-app/src/main/java/org/springframework/boot/consoleapp/ConsoleTestApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.consoleapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.ansi.AnsiOutput.Enabled; + +@SpringBootApplication +public class ConsoleTestApplication { + + public static void main(String[] args) { + System.out.println("System.console() is " + System.console()); + SpringApplication.run(ConsoleTestApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/java/org/springframework/boot/console/ConsoleIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/java/org/springframework/boot/console/ConsoleIntegrationTests.java new file mode 100644 index 00000000000..05eff35a829 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/java/org/springframework/boot/console/ConsoleIntegrationTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.console; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import org.springframework.boot.system.JavaVersion; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests that checks ANSI output is not turned on in JDK 22 or later. + * + * @author Yong-Hyun Kim + */ +@DisabledIfDockerUnavailable +class ConsoleIntegrationTests { + + private static final String ENCODE_START = "\033["; + + private static final JavaRuntime JDK_17_RUNTIME = JavaRuntime.openJdk(JavaVersion.SEVENTEEN); + + private static final JavaRuntime JDK_22_RUNTIME = JavaRuntime.openJdk(JavaVersion.TWENTY_TWO); + + private final ToStringConsumer output = new ToStringConsumer().withRemoveAnsiCodes(false); + + @Test + void runJarOn17() { + try (GenericContainer container = createContainer(JDK_17_RUNTIME)) { + container.start(); + assertThat(this.output.toString(StandardCharsets.ISO_8859_1)).contains("System.console() is null") + .doesNotContain(ENCODE_START); + } + } + + @Test + void runJarOn22() { + try (GenericContainer container = createContainer(JDK_22_RUNTIME)) { + container.start(); + assertThat(this.output.toString(StandardCharsets.ISO_8859_1)).doesNotContain("System.console() is null") + .doesNotContain(ENCODE_START); + } + } + + private GenericContainer createContainer(JavaRuntime javaRuntime) { + return javaRuntime.getContainer() + .withLogConsumer(this.output) + .withCopyFileToContainer(findApplication(), "/app.jar") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5))) + .withCommand("java", "-jar", "app.jar"); + } + + private MountableFile findApplication() { + return MountableFile.forHostPath(findJarFile().toPath()); + } + + private File findJarFile() { + String path = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-console-tests-app"); + File jar = new File(path); + Assert.state(jar.isFile(), () -> "Could not find " + path + ". Have you built it?"); + return jar; + } + + static final class JavaRuntime { + + private final String name; + + private final JavaVersion version; + + private final Supplier> container; + + private JavaRuntime(String name, JavaVersion version, Supplier> container) { + this.name = name; + this.version = version; + this.container = container; + } + + private boolean isCompatible() { + return this.version.isEqualOrNewerThan(JavaVersion.getJavaVersion()); + } + + GenericContainer getContainer() { + return this.container.get(); + } + + @Override + public String toString() { + return this.name; + } + + static JavaRuntime openJdk(JavaVersion version) { + String imageVersion = version.toString(); + DockerImageName image = DockerImageName.parse("bellsoft/liberica-openjdk-debian:" + imageVersion); + return new JavaRuntime("OpenJDK " + imageVersion, version, () -> new GenericContainer<>(image)); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/resources/logback.xml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/resources/logback.xml new file mode 100644 index 00000000000..b8a41480d7d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-console-tests/src/intTest/resources/logback.xml @@ -0,0 +1,4 @@ + + + +