diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailable.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailable.java new file mode 100644 index 0000000000..faac6707bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailable.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 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.testsupport.process; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Disables test execution if a process is unavailable. + * + * @author Phillip Webb + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledIfProcessUnavailableCondition.class) +@Repeatable(DisabledIfProcessUnavailables.class) +public @interface DisabledIfProcessUnavailable { + + String[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java new file mode 100644 index 0000000000..d45e3f9708 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2023 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.testsupport.process; + +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An {@link ExecutionCondition} that disables execution if specified processes cannot + * start. + * + * @author Phillip Webb + */ +class DisabledIfProcessUnavailableCondition implements ExecutionCondition { + + private static final String USR_LOCAL_BIN = "/usr/local/bin"; + + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase().contains("mac"); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + List commands = new ArrayList<>(); + context.getTestClass().map(this::getAnnotationValue).orElse(Stream.empty()).forEach(commands::add); + context.getTestMethod().map(this::getAnnotationValue).orElse(Stream.empty()).forEach(commands::add); + try { + commands.forEach(this::check); + return ConditionEvaluationResult.enabled("All processes available"); + } + catch (Throwable ex) { + return ConditionEvaluationResult.disabled("Process unavailable", ex.getMessage()); + } + } + + private Stream getAnnotationValue(AnnotatedElement testClass) { + return MergedAnnotations.from(testClass) + .stream(DisabledIfProcessUnavailable.class) + .map((annotation) -> annotation.getStringArray(MergedAnnotation.VALUE)); + } + + private void check(String[] command) { + ProcessBuilder processBuilder = new ProcessBuilder(command); + try { + Process process = processBuilder.start(); + process.waitFor(); + Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue())); + process.destroy(); + } + catch (Exception ex) { + String path = processBuilder.environment().get("PATH"); + if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) + && !command[0].startsWith(USR_LOCAL_BIN + "/")) { + String[] localCommand = command.clone(); + localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; + check(localCommand); + return; + } + throw new RuntimeException( + "Unable to start process '%s'".formatted(StringUtils.arrayToDelimitedString(command, " "))); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailables.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailables.java new file mode 100644 index 0000000000..7804d65634 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailables.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 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.testsupport.process; + +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 org.junit.jupiter.api.extension.ExtendWith; + +/** + * Repeatable container for {@link DisabledIfProcessUnavailables}. + * + * @author Phillip Webb + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledIfProcessUnavailableCondition.class) +public @interface DisabledIfProcessUnavailables { + + DisabledIfProcessUnavailable[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/package-info.java new file mode 100644 index 0000000000..199fe51f6f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Classes to help when shelling out to processes in tests. + */ +package org.springframework.boot.testsupport.process; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableTests.java new file mode 100644 index 0000000000..633ad6d847 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2023 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.testsupport.process; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link DisabledIfProcessUnavailable}. + * + * @author Phillip Webb + */ +@DisabledIfProcessUnavailable("iverymuchdontexist") +class DisabledIfProcessUnavailableTests { + + @Test + void test() { + fail("I should have been disabled"); + } + +}