commit 82d82b0980ae979311fbc90a809c5e697e9024f5 Author: Sébastien Deleuze Date: Tue Jun 27 12:48:52 2023 +0200 Initial commit The bootstrap of this project is done by leveraging and adapting the infrastructure created in https://github.com/spring-projects/spring-aot-smoke-tests. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64c0322 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.settings/ +.project +.classpath +build/ +bin/ +.gradle/ +.idea/ +out/ +ci/pipeline.yml + diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 0000000..2b1efe7 --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,16 @@ += Contributing + +Before submitting a pull request, please make sure that: + +1. You've rebased your changes on top of the current `main` branch +2. If you created a new top level directory: You've included the new directory in `settings.gradle` +3. You've run `./gradlew updateInfrastructure` +4. You've run `./gradlew :::build` + +== Docker Compose tips + +If your smoke test uses Docker Compose here are few tips: + +* Wrap your port values in single quotes (`'8080'`) +* Do not specify hardcoded port mappings in `docker-compose.yml` - use `'8080'` rather than `'8080':'8080'` +* Configure your sample app to use the dynamic host (`YOUR_SVC_HOST`) and port (`YOUR_SVC_PORT`) \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..823c1c8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. \ No newline at end of file diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..b40f8a1 --- /dev/null +++ b/README.adoc @@ -0,0 +1,97 @@ += Spring Checkpoint Restore Smoke Tests + +A suite of tests for applications using checkpoint/restore as implemented by https://github.com/CRaC/docs[Project CRaC] and as documented in https://docs.spring.io/spring-framework/reference/6.1/integration/checkpoint-restore.html[Spring Framework reference documentation]. +There are two types of tests: unit tests and application tests. + +Unit tests can be run using the `test` task. + +The `appTest` task tests the application running on the JVM. + +== Contributing + +Please read and follow the link:CONTRIBUTING.adoc[contributing guide]. + +== How to + +=== Run all of a project's smoke tests + +[source,] +---- +./gradlew :::build +---- + +for example + +[source,] +---- +./gradlew :boot:actuator-webmvc:build +---- + +=== Run a specific type of tests for a project + +[source,] +---- +./gradlew ::: +---- + +Valid test task names are: + +1. `appTest` – tests the application running on the JVM +2. `test` – executes the unit tests on the JVM + +for example + +[source,] +---- +./gradlew :boot:actuator-webmvc:appTest +---- + +=== Add a new smoke test + +1. Create a new directory for your smoke test in the appropriate group +2. Include the directory in `settings.gradle` (new groups only) +3. Run `./gradlew updateInfrastructure` to add the smoke test to the status page and CI pipeline + +=== Test against local changes + +==== Your project uses Gradle + +[source,] +---- +./gradlew :::build --include-build /path/to/your/project +---- + +Gradle https://docs.gradle.org/current/userguide/composite_builds.html#command_line_composite[will then substitute the dependency] with your provided version. + +_Hint: You can use `--include-build` multiple times._ + +==== Your project uses Maven or --include-build does not work + +First, install the snapshots into your local Maven cache. +You can now consume those snapshots using `-PfromMavenLocal` which takes a comma-separated list of group IDs: + +[source,] +---- +./gradlew :rest-template:build -PfromMavenLocal=org.springframework,org.springframework.data +---- + +The preceding example will run the `rest-template` smoke test, resolving Spring Framework and Spring Data modules from your local Maven cache. + +=== Override a dependency version + +As the test doesn't use the Spring Dependency Management Plugin, you can't use the `ext['...'] = '...'` method. + +Instead, use https://docs.gradle.org/current/userguide/dependency_constraints.html[Gradle dependency constraints]. +Say, for example, you want to update the version of Spring Session JDBC to `3.0.0-SNAPSHOT`: + +[source,] +---- +dependencies { + // ... + constraints { + implementation('org.springframework.session:spring-session-jdbc:3.0.0-SNAPSHOT') + } +} +---- + +This works for direct and transitive dependencies. diff --git a/STATUS.adoc b/STATUS.adoc new file mode 100644 index 0000000..9255233 --- /dev/null +++ b/STATUS.adoc @@ -0,0 +1,18 @@ += Smoke Tests Status +:toc: +:toc-title: Projects + +== Framework + +[%header,cols="3"] +|=== +h|Smoke Test +h|appTest +h|test + +|webmvc-tomcat +|image:https://ci.spring.io/api/v1/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/webmvc-tomcat-app-test/badge[link=https://ci.spring.io/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/webmvc-tomcat-app-test] +|image:https://ci.spring.io/api/v1/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/webmvc-tomcat-test/badge[link=https://ci.spring.io/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/webmvc-tomcat-test] + +|=== + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7d5993c --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +buildscript { + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" + } + configurations.classpath { + resolutionStrategy.useGlobalDependencySubstitutionRules = false + } +} + +plugins { + id "io.spring.javaformat" version "$javaFormatVersion" apply false + id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" apply false + id "org.springframework.cr.smoke-test-aggregator" +} + +subprojects { + plugins.withType(JavaPlugin) { + plugins.apply("io.spring.javaformat") + } +} diff --git a/ci/smoke-tests.yml b/ci/smoke-tests.yml new file mode 100644 index 0000000..29b3df0 --- /dev/null +++ b/ci/smoke-tests.yml @@ -0,0 +1,6 @@ +groups: + - name: framework + smoke_tests: + - name: webmvc-tomcat + app_test: true + test: true diff --git a/cr-smoke-test-support/build.gradle b/cr-smoke-test-support/build.gradle new file mode 100644 index 0000000..472cf09 --- /dev/null +++ b/cr-smoke-test-support/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "java-library" + id "io.spring.javaformat" + id "org.springframework.boot" apply false +} + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/milestone" } + maven { url "https://repo.spring.io/snapshot" } +} + +dependencies { + api(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + runtimeOnly("io.projectreactor.netty:reactor-netty-http") + api("org.springframework:spring-webflux") + api("org.springframework.boot:spring-boot-starter-test") +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/Output.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/Output.java new file mode 100644 index 0000000..f742947 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/Output.java @@ -0,0 +1,57 @@ +/* + * Copyright 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.cr.smoketest.support; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Output from an application. + * + * @author Andy Wilkinson + */ +public class Output { + + private final Path path; + + private Output(Path path) { + this.path = path; + } + + public List lines() { + try { + return Files.readAllLines(this.path); + } + catch (IOException ex) { + throw new RuntimeException(); + } + } + + public static Output current() { + String property = System.getProperty("org.springframework.cr.smoketest.standard-output"); + if (property == null) { + throw new IllegalStateException( + "Standard output is not available as org.springframework.cr.smoketest.standard-output " + + "system property has not been set"); + } + return new Output(new File(property).toPath()); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/assertj/AssertableOutput.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/assertj/AssertableOutput.java new file mode 100644 index 0000000..64a6f30 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/assertj/AssertableOutput.java @@ -0,0 +1,37 @@ +/* + * Copyright 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.cr.smoketest.support.assertj; + +import org.assertj.core.api.AssertProvider; + +import org.springframework.cr.smoketest.support.Output; + +/** + * An {@link AssertProvider} for {@link OutputAssert}. + * + * @author Andy Wilkinson + */ +public class AssertableOutput implements AssertProvider { + + private final Output output = Output.current(); + + @Override + public OutputAssert assertThat() { + return new OutputAssert(this.output); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/assertj/OutputAssert.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/assertj/OutputAssert.java new file mode 100644 index 0000000..58d9317 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/assertj/OutputAssert.java @@ -0,0 +1,99 @@ +/* + * Copyright 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.cr.smoketest.support.assertj; + +import java.util.List; +import java.util.Optional; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.error.BasicErrorMessageFactory; + +import org.springframework.cr.smoketest.support.Output; + +/** + * An {@link AbstractAssert} for {@link Output}. + * + * @author Andy Wilkinson + */ +public class OutputAssert extends AbstractAssert { + + OutputAssert(Output actual) { + super(actual, OutputAssert.class); + } + + /** + * Asserts that the output has a single line matching the given contents. + * @param contents contents to match + * @return {@code this} for fluent API + */ + public OutputAssert hasSingleLineContaining(String contents) { + List lines = this.actual.lines(); + List matchingLines = lines.stream().filter((line) -> line.contains(contents)).toList(); + if (matchingLines.size() != 1) { + throwAssertionError( + new BasicErrorMessageFactory("%nExpected %s to have a single line that contains '%s' but found %s", + lines, contents, matchingLines.size())); + } + return this; + } + + /** + * Asserts that the output has a line matching the given contents. + * @param contents contents to match + * @return {@code this} for fluent API + */ + public OutputAssert hasLineContaining(String contents) { + List lines = this.actual.lines(); + Optional matchingLines = lines.stream().filter((line) -> line.contains(contents)).findAny(); + if (matchingLines.isEmpty()) { + throwAssertionError(new BasicErrorMessageFactory( + "%nExpected %s to have a line that contains '%s' but found none", lines, contents)); + } + return this; + } + + /** + * Asserts that the output has a line matching the given contents. + * @param regex contents to match + * @return {@code this} for fluent API + */ + public OutputAssert hasLineMatching(String regex) { + List lines = this.actual.lines(); + Optional matchingLines = lines.stream().filter((line) -> line.matches(regex)).findAny(); + if (matchingLines.isEmpty()) { + throwAssertionError(new BasicErrorMessageFactory( + "%nExpected %s to have a line that matches '%s' but found none", lines, regex)); + } + return this; + } + + /** + * Asserts that the output doesn't have a line matching the given contents. + * @param contents contents to match + * @return {@code this} for fluent API + */ + public OutputAssert hasNoLinesContaining(String contents) { + List lines = this.actual.lines(); + boolean noMatch = lines.stream().noneMatch((line) -> line.contains(contents)); + if (!noMatch) { + throwAssertionError(new BasicErrorMessageFactory( + "%nExpected %s to have no lines that contain '%s' but found some", lines, contents)); + } + return this; + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationTest.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationTest.java new file mode 100644 index 0000000..b25e71a --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.net.URI; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extensions; + +import org.springframework.cr.smoketest.support.assertj.AssertableOutput; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * JUnit Jupiter {@link ExtendWith extension} for an application test where the + * application runs on the JVM. Allows the following types to be injected as parameters: + * + *
    + *
  • {@link AssertableOutput} - application's redirected output
  • + *
  • {@link WebTestClient} - preconfigured with a base URL for the application's + * port
  • + *
  • {@link DockerComposeHost} annotated String - host name of the + * docker-compose service
  • + *
  • {@link DockerComposePort} annotated int - port of the docker-compose + * service
  • + *
  • {@link ApplicationUrl} annotated {@link URI} - URI to the application
  • + * + * @author Andy Wilkinson + * @author Moritz Halbritter + */ +@Retention(RUNTIME) +@Target(TYPE) +@Extensions({ @ExtendWith(AssertableOutputParameterResolver.class), @ExtendWith(WebTestClientParameterResolver.class), + @ExtendWith(DockerComposeHostParameterResolver.class), @ExtendWith(DockerComposePortParameterResolver.class), + @ExtendWith(ApplicationUrlParameterResolver.class), @ExtendWith(AwaitApplication.class) }) +public @interface ApplicationTest { + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUnderTest.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUnderTest.java new file mode 100644 index 0000000..534b1c8 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUnderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; + +import org.springframework.cr.smoketest.support.Output; + +/** + * Provides access to data about the application which is being tested. + * + * @author Moritz Halbritter + */ +final class ApplicationUnderTest { + + private ApplicationUnderTest() { + } + + private int port = -1; + + /** + * Gets the port of the application. + * @return the application port + */ + int getPort() { + if (this.port != -1) { + return this.port; + } + List portPatterns = List.of(Pattern.compile("Tomcat started on port\\(s\\): ([0-9]+)"), + Pattern.compile("Netty started on port ([0-9]+)"), + Pattern.compile("Jetty started on port\\(s\\) ([0-9]+)"), + Pattern.compile("Undertow started on port\\(s\\) ([0-9]+)")); + List lines = Output.current().lines(); + for (String line : lines) { + for (Pattern portPattern : portPatterns) { + Matcher matcher = portPattern.matcher(line); + if (matcher.find()) { + this.port = Integer.parseInt(matcher.group(1)); + return this.port; + } + } + } + StringBuilder message = new StringBuilder("Port log message was not found in output:"); + message.append("\n\n"); + if (lines.isEmpty()) { + message.append("<< none >>"); + } + else { + for (String line : lines) { + message.append(line).append("\n"); + } + } + message.append("\n"); + throw new IllegalStateException(message.toString()); + } + + /** + * Returns the current {@link ApplicationUnderTest} for the given + * {@link ExtensionContext}. Never returns null. + * @param extensionContext JUnit extension context + * @return current {@link ApplicationUnderTest} + */ + static ApplicationUnderTest get(ExtensionContext extensionContext) { + Store store = extensionContext.getStore(Namespace.create(ApplicationUnderTest.class)); + return (ApplicationUnderTest) store.getOrComputeIfAbsent(ApplicationUnderTest.class, + (c) -> new ApplicationUnderTest()); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUrl.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUrl.java new file mode 100644 index 0000000..f8609a5 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUrl.java @@ -0,0 +1,59 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.net.URI; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * {@link URI} parameters of JUnit test methods annotated with this annotation will be + * resolved to the URI for the running Spring Boot application. + * + * @author Moritz Halbritter + */ +@Retention(RUNTIME) +@Target({ FIELD, PARAMETER }) +public @interface ApplicationUrl { + + /** + * Scheme of the produced {@link URI}. + * @return the scheme + */ + Scheme scheme() default Scheme.HTTP; + + enum Scheme { + + WEBSOCKET("ws"), SECURE_WEBSOCKET("wss"), HTTP("http"), HTTPS("https"); + + private final String scheme; + + Scheme(String scheme) { + this.scheme = scheme; + } + + String getScheme() { + return scheme; + } + + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUrlParameterResolver.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUrlParameterResolver.java new file mode 100644 index 0000000..01af99d --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/ApplicationUrlParameterResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.net.URI; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * {@link ParameterResolver} for {@link ApplicationUrl} annotated {@link URI}s. + * + * @author Moritz Halbritter + */ +public class ApplicationUrlParameterResolver implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return URI.class.equals(parameterContext.getParameter().getType()) + && parameterContext.isAnnotated(ApplicationUrl.class); + + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + ApplicationUrl annotation = parameterContext.findAnnotation(ApplicationUrl.class).get(); + return URI.create(annotation.scheme().getScheme() + "://localhost:" + + ApplicationUnderTest.get(extensionContext).getPort()); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/AssertableOutputParameterResolver.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/AssertableOutputParameterResolver.java new file mode 100644 index 0000000..4a750de --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/AssertableOutputParameterResolver.java @@ -0,0 +1,44 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import org.springframework.cr.smoketest.support.assertj.AssertableOutput; + +/** + * {@link ParameterResolver} for {@link AssertableOutput}. + * + * @author Andy Wilkinson + */ +class AssertableOutputParameterResolver implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return AssertableOutput.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return new AssertableOutput(); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/AwaitApplication.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/AwaitApplication.java new file mode 100644 index 0000000..b99ffb6 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/AwaitApplication.java @@ -0,0 +1,70 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.cr.smoketest.support.Output; + +/** + * {@link BeforeAllCallback} that waits for the application to have started. + * + * @author Andy Wilkinson + */ +class AwaitApplication implements BeforeAllCallback { + + private static final Duration START_TIMEOUT = Duration.ofSeconds(30); + + private Pattern APPLICATION_STARTED = Pattern + .compile("Started [A-Za-z0-9]+ in [0-9\\.]+ seconds \\(process running for [0-9\\.]+\\)"); + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + Output output = Output.current(); + long end = Instant.now().plus(START_TIMEOUT).toEpochMilli(); + List lines = null; + while (System.currentTimeMillis() < end) { + lines = output.lines(); + for (String line : lines) { + if (this.APPLICATION_STARTED.matcher(line).find()) { + return; + } + } + } + StringBuilder message = new StringBuilder( + "Started log message was not detected within " + START_TIMEOUT + " in output:"); + message.append("\n\n"); + if (lines == null || lines.isEmpty()) { + message.append("<< none >>"); + } + else { + for (String line : lines) { + message.append(line + "\n"); + } + } + message.append("\n"); + System.err.println(message.toString()); + throw new IllegalStateException(message.toString()); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposeHost.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposeHost.java new file mode 100644 index 0000000..bc2971e --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposeHost.java @@ -0,0 +1,42 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * String parameters of JUnit test methods annotated with this annotation + * will be resolved to the docker-compose host name for a given service. + * + * @author Moritz Halbritter + */ +@Retention(RUNTIME) +@Target({ FIELD, PARAMETER }) +public @interface DockerComposeHost { + + /** + * Name of the docker-compose service. + * @return name of the docker-compose service. + */ + String value(); + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposeHostParameterResolver.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposeHostParameterResolver.java new file mode 100644 index 0000000..28e74a7 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposeHostParameterResolver.java @@ -0,0 +1,55 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.util.Locale; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * {@link ParameterResolver} for {@link DockerComposeHost} annotated Strings. + * + * @author Moritz Halbritter + */ +class DockerComposeHostParameterResolver implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return String.class.equals(parameterContext.getParameter().getType()) + && parameterContext.isAnnotated(DockerComposeHost.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + DockerComposeHost annotation = parameterContext.findAnnotation(DockerComposeHost.class).get(); + String serviceName = annotation.value(); + String envVariable = serviceName.toUpperCase(Locale.ENGLISH) + "_HOST"; + String value = System.getenv(envVariable); + if (value == null) { + throw new ParameterResolutionException( + "Can't get host name for docker-compose service '%s', the environment variable '%s' is missing" + .formatted(serviceName, envVariable)); + } + return value; + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposePort.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposePort.java new file mode 100644 index 0000000..cb0b009 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposePort.java @@ -0,0 +1,48 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * int parameters of JUnit test methods annotated with this annotation will + * be resolved to the docker-compose port for a given service. + * + * @author Moritz Halbritter + */ +@Retention(RUNTIME) +@Target({ FIELD, PARAMETER }) +public @interface DockerComposePort { + + /** + * Name of the docker-compose service. + * @return name of the docker-compose service. + */ + String service(); + + /** + * Port number of the docker-compose service. + * @return port number of the docker-compose service. + */ + int port(); + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposePortParameterResolver.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposePortParameterResolver.java new file mode 100644 index 0000000..825fa80 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/DockerComposePortParameterResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import java.util.Locale; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * {@link ParameterResolver} for {@link DockerComposePort} annotated ints. + * + * @author Moritz Halbritter + */ +class DockerComposePortParameterResolver implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return int.class.equals(parameterContext.getParameter().getType()) + && parameterContext.isAnnotated(DockerComposePort.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + DockerComposePort annotation = parameterContext.findAnnotation(DockerComposePort.class).get(); + String serviceName = annotation.service(); + int port = annotation.port(); + String envVariable = serviceName.toUpperCase(Locale.ENGLISH) + "_PORT_" + port; + String value = System.getenv(envVariable); + if (value == null) { + throw new ParameterResolutionException( + "Can't get mapped port for docker-compose service '%s' port %d, the environment variable '%s' is missing" + .formatted(serviceName, port, envVariable)); + } + return Integer.parseInt(value); + } + +} diff --git a/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/WebTestClientParameterResolver.java b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/WebTestClientParameterResolver.java new file mode 100644 index 0000000..ff18773 --- /dev/null +++ b/cr-smoke-test-support/src/main/java/org/springframework/cr/smoketest/support/junit/WebTestClientParameterResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright 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.cr.smoketest.support.junit; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.Builder; + +/** + * A {@link ParameterResolver} for {@link WebTestClient}. The {@code WebTestClient} is + * built with a {@link Builder#baseUrl(String) base URL} for the application under test. + * + * @author Andy Wilkinson + */ +class WebTestClientParameterResolver implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return WebTestClient.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Store store = extensionContext.getRoot().getStore(Namespace.create(WebTestClient.class)); + return store.getOrComputeIfAbsent(WebTestClient.class, (c) -> createWebTestClient(extensionContext)); + } + + private WebTestClient createWebTestClient(ExtensionContext extensionContext) { + return WebTestClient.bindToServer() + .baseUrl("http://localhost:" + ApplicationUnderTest.get(extensionContext).getPort()) + .build(); + } + +} diff --git a/framework/webmvc-tomcat/README.adoc b/framework/webmvc-tomcat/README.adoc new file mode 100644 index 0000000..100808c --- /dev/null +++ b/framework/webmvc-tomcat/README.adoc @@ -0,0 +1 @@ +Tests if WebMVC with Tomcat is working. diff --git a/framework/webmvc-tomcat/build.gradle b/framework/webmvc-tomcat/build.gradle new file mode 100644 index 0000000..e336ecd --- /dev/null +++ b/framework/webmvc-tomcat/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-web") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} diff --git a/framework/webmvc-tomcat/src/appTest/java/com/example/webmvc/WebMvcApplicationTests.java b/framework/webmvc-tomcat/src/appTest/java/com/example/webmvc/WebMvcApplicationTests.java new file mode 100644 index 0000000..22b8e38 --- /dev/null +++ b/framework/webmvc-tomcat/src/appTest/java/com/example/webmvc/WebMvcApplicationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 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 com.example.webmvc; + +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +class WebMvcApplicationTests { + + @Test + void stringResponseBody(WebTestClient client) { + client.get() + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())) + .isEqualTo("Hello from Spring MVC and Tomcat")); + } + + @Test + void resourceInStatic(WebTestClient client) { + client.get() + .uri("foo.html") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("Foo")); + } + +} diff --git a/framework/webmvc-tomcat/src/main/java/com/example/webmvc/WebMvcApplication.java b/framework/webmvc-tomcat/src/main/java/com/example/webmvc/WebMvcApplication.java new file mode 100644 index 0000000..d1ca57e --- /dev/null +++ b/framework/webmvc-tomcat/src/main/java/com/example/webmvc/WebMvcApplication.java @@ -0,0 +1,13 @@ +package com.example.webmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebMvcApplication { + + public static void main(String[] args) { + SpringApplication.run(WebMvcApplication.class, args); + } + +} diff --git a/framework/webmvc-tomcat/src/main/java/com/example/webmvc/WebMvcController.java b/framework/webmvc-tomcat/src/main/java/com/example/webmvc/WebMvcController.java new file mode 100644 index 0000000..522f495 --- /dev/null +++ b/framework/webmvc-tomcat/src/main/java/com/example/webmvc/WebMvcController.java @@ -0,0 +1,14 @@ +package com.example.webmvc; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WebMvcController { + + @GetMapping("/") + public String hello() { + return "Hello from Spring MVC and Tomcat"; + } + +} diff --git a/framework/webmvc-tomcat/src/main/resources/static/foo.html b/framework/webmvc-tomcat/src/main/resources/static/foo.html new file mode 100644 index 0000000..9f26b63 --- /dev/null +++ b/framework/webmvc-tomcat/src/main/resources/static/foo.html @@ -0,0 +1 @@ +Foo \ No newline at end of file diff --git a/framework/webmvc-tomcat/src/test/java/com/example/webmvc/RandomPortTests.java b/framework/webmvc-tomcat/src/test/java/com/example/webmvc/RandomPortTests.java new file mode 100644 index 0000000..9d6abe7 --- /dev/null +++ b/framework/webmvc-tomcat/src/test/java/com/example/webmvc/RandomPortTests.java @@ -0,0 +1,40 @@ +package com.example.webmvc; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class RandomPortTests { + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate template; + + @Test + void check() { + assertThat(this.port).isNotZero(); + } + + @Test + void stringResponseBody() { + ResponseEntity response = this.template.getForEntity("/", String.class); + assertThat(response.getBody()).isEqualTo("Hello from Spring MVC and Tomcat"); + } + + @Test + void resourceInStatic() { + ResponseEntity response = this.template.getForEntity("/foo.html", String.class); + assertThat(response.getBody()).isEqualTo("Foo"); + } + +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ec00c67 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2g + +kotlinVersion=1.8.22 +javaFormatVersion=0.0.38 +springBootVersion=3.2.0-SNAPSHOT diff --git a/gradle/plugins/cr-smoke-test-plugin/build.gradle b/gradle/plugins/cr-smoke-test-plugin/build.gradle new file mode 100644 index 0000000..7e81faf --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/build.gradle @@ -0,0 +1,47 @@ +plugins { + id "checkstyle" + id "io.spring.javaformat" + id "java-gradle-plugin" +} + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/milestone" } + maven { url "https://repo.spring.io/snapshot" } +} + +gradlePlugin { + plugins { + crSmokeTestPlugin { + id = "org.springframework.cr.smoke-test" + implementationClass = "org.springframework.cr.gradle.CrSmokeTestPlugin" + } + crSmokeTestAggregatorPlugin { + id = "org.springframework.cr.smoke-test-aggregator" + implementationClass = "org.springframework.cr.gradle.CrSmokeTestAggregatorPlugin" + } + } +} + +new File(rootDir.parentFile.parentFile.parentFile, "gradle.properties").withInputStream { + def properties = new Properties() + properties.load(it) + properties.each { key, value -> + if (key.endsWith("Version")) { + project.ext.set(key, value) + } + } +} + +dependencies { + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:$javaFormatVersion") + + compileOnly("io.spring.javaformat:spring-javaformat-gradle-plugin:$javaFormatVersion") + compileOnly("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + + implementation("com.avast.gradle:gradle-docker-compose-plugin:0.16.8") +} + +sourceCompatibility = "17" +targetCompatibility = "17" diff --git a/gradle/plugins/cr-smoke-test-plugin/config/checkstyle/checkstyle.xml b/gradle/plugins/cr-smoke-test-plugin/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..f8fd854 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/config/checkstyle/checkstyle.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/gradle/plugins/cr-smoke-test-plugin/settings.gradle b/gradle/plugins/cr-smoke-test-plugin/settings.gradle new file mode 100644 index 0000000..417c5d9 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + new File(rootDir.parentFile.parentFile.parentFile, "gradle.properties").withInputStream { + def properties = new Properties() + properties.load(it) + properties.each { key, value -> + if (key.endsWith("Version")) { + gradle.extensions.extraProperties.set(key, value) + } + } + } + plugins { + id "io.spring.javaformat" version gradle.extensions.extraProperties.get("javaFormatVersion") + } +} + +rootProject.name="cr-smoke-test-plugin" diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/CrSmokeTestAggregatorPlugin.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/CrSmokeTestAggregatorPlugin.java new file mode 100644 index 0000000..a30721b --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/CrSmokeTestAggregatorPlugin.java @@ -0,0 +1,53 @@ +/* + * Copyright 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.cr.gradle; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.tasks.TaskProvider; + +import org.springframework.cr.gradle.tasks.UpdateConcoursePipeline; +import org.springframework.cr.gradle.tasks.UpdateStatusPage; + +/** + * Plugin for a project that aggregates the smoke tests to provide status and a CI + * pipeline. + * + * @author Andy Wilkinson + * @author Sebastien Deleuze + */ +public class CrSmokeTestAggregatorPlugin implements Plugin { + + @Override + public void apply(Project project) { + Configuration smokeTests = project.getConfigurations().create("smokeTests"); + TaskProvider updateStatusPage = project.getTasks() + .register("updateStatusPage", UpdateStatusPage.class, (task) -> { + task.setSmokeTests(smokeTests); + task.getOutputFile().set(project.file("STATUS.adoc")); + }); + TaskProvider updateConcoursePipeline = project.getTasks() + .register("updateConcoursePipeline", UpdateConcoursePipeline.class, (task) -> { + task.setSmokeTests(smokeTests); + task.getOutputFile().set(project.file("ci/smoke-tests.yml")); + }); + project.getTasks() + .register("updateInfrastructure", (task) -> task.dependsOn(updateStatusPage, updateConcoursePipeline)); + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/CrSmokeTestPlugin.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/CrSmokeTestPlugin.java new file mode 100644 index 0000000..a4780bc --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/CrSmokeTestPlugin.java @@ -0,0 +1,251 @@ +/* + * Copyright 2022 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.cr.gradle; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Stream; + +import com.avast.gradle.dockercompose.ComposeExtension; +import com.avast.gradle.dockercompose.ComposeSettings; +import org.gradle.api.JavaVersion; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.Directory; +import org.gradle.api.file.RegularFile; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile; + +import org.springframework.boot.gradle.plugin.SpringBootPlugin; +import org.springframework.boot.gradle.tasks.bundling.BootJar; +import org.springframework.cr.gradle.dsl.CrSmokeTestExtension; +import org.springframework.cr.gradle.tasks.AppTest; +import org.springframework.cr.gradle.tasks.DescribeSmokeTests; +import org.springframework.cr.gradle.tasks.StartApplication; +import org.springframework.cr.gradle.tasks.StartJvmApplication; +import org.springframework.cr.gradle.tasks.StopApplication; + +/** + * {@link Plugin} for a checkpoint/restore smoke test project. Configures an {@code appTest} source set + * and tasks for running the contained tests against the application running on the JVM + * and as a native image. + * + * @author Andy Wilkinson + * @author Moritz Halbritter + * @author Sebastien Deleuze + */ +public class CrSmokeTestPlugin implements Plugin { + + @Override + public void apply(Project project) { + project.getPlugins() + .withType(JavaPlugin.class, (javaPlugin) -> project.getPlugins() + .withType(SpringBootPlugin.class, (bootPlugin) -> configureBootJavaProject(project))); + } + + private void configureBootJavaProject(Project project) { + CrSmokeTestExtension extension = project.getExtensions() + .create("crSmokeTest", CrSmokeTestExtension.class, project); + extension.getWebApplication().convention(false); + JavaPluginExtension javaExtension = project.getExtensions().getByType(JavaPluginExtension.class); + SourceSet appTest = javaExtension.getSourceSets().create("appTest"); + javaExtension.setSourceCompatibility(JavaVersion.VERSION_17); + javaExtension.setTargetCompatibility(JavaVersion.VERSION_17); + if (project.hasProperty("fromMavenLocal")) { + String fromMavenLocal = project.property("fromMavenLocal").toString(); + Stream includedGroups = Stream.of(fromMavenLocal.split(",")); + project.getRepositories() + .mavenLocal( + (mavenLocal) -> mavenLocal.content((content) -> includedGroups.forEach(content::includeGroup))); + } + project.getRepositories().mavenCentral(); + project.getRepositories().maven((repo) -> { + repo.setName("Spring Milestone"); + repo.setUrl("https://repo.spring.io/milestone"); + }); + project.getRepositories().maven((repo) -> { + repo.setName("Spring Snapshot"); + repo.setUrl("https://repo.spring.io/snapshot"); + }); + configureAppTests(project, extension, appTest); + configureTests(project); + configureKotlin(project, javaExtension); + Configuration smokeTests = project.getConfigurations().create("smokeTests"); + TaskProvider describeSmokeTests = project.getTasks() + .register("describeSmokeTests", DescribeSmokeTests.class); + describeSmokeTests.configure((task) -> { + task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(task.getName())); + task.setAppTests(appTest.getAllSource()); + task.setTests(javaExtension.getSourceSets().getByName(SourceSet.TEST_SOURCE_SET_NAME).getAllSource()); + }); + project.artifacts((artifacts) -> artifacts.add(smokeTests.getName(), describeSmokeTests)); + DependencyHandler dependencies = project.getRootProject().getDependencies(); + dependencies.add("smokeTests", + dependencies.project(Map.of("path", project.getPath(), "configuration", smokeTests.getName()))); + } + + private void configureAppTests(Project project, CrSmokeTestExtension extension, SourceSet appTest) { + configureJvmAppTests(project, appTest, extension); + } + + private void configureTests(Project project) { + project.getTasks().named(JavaPlugin.TEST_TASK_NAME, Test.class).configure((test) -> { + test.useJUnitPlatform(); + }); + } + + private void configureKotlin(Project project, JavaPluginExtension javaExtension) { + project.getTasks() + .withType(KotlinJvmCompile.class) + .configureEach((kotlinCompile) -> kotlinCompile.getKotlinOptions() + .setJvmTarget(javaExtension.getTargetCompatibility().toString())); + } + + private void configureJvmAppTests(Project project, SourceSet sourceSet, CrSmokeTestExtension extension) { + Provider archiveFile = project.getTasks() + .named(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class) + .flatMap(BootJar::getArchiveFile); + configureTasks(project, sourceSet, ApplicationType.JVM, archiveFile, extension); + } + + private TaskProvider configureTasks(Project project, SourceSet appTest, ApplicationType type, + Provider nativeImage, CrSmokeTestExtension extension) { + Provider outputDirectory = project.getLayout() + .getBuildDirectory() + .dir(type.name().toLowerCase() + "App"); + TaskProvider startTask = createStartApplicationTask(project, type, nativeImage, + outputDirectory, extension); + TaskProvider stopTask = createStopApplicationTask(project, type, startTask); + TaskProvider appTestTask = createAppTestTask(project, appTest, type, startTask, stopTask); + configureDockerComposeIfNecessary(project, type, startTask, appTestTask, stopTask); + return appTestTask; + } + + private void configureDockerComposeIfNecessary(Project project, ApplicationType type, + TaskProvider startTask, TaskProvider appTestTask, + TaskProvider stopTask) { + if (!project.file("docker-compose.yml").canRead()) { + return; + } + project.getPlugins().apply("com.avast.gradle.docker-compose"); + ComposeExtension compose = project.getExtensions().getByType(ComposeExtension.class); + ComposeSettings composeSettings = compose.nested(type.name().toLowerCase()); + String composeUpTaskName = composeSettings.getNestedName() + "ComposeUp"; + String composeDownTaskName = composeSettings.getNestedName() + "ComposeDown"; + project.getTasks() + .named(composeUpTaskName) + .configure((composeUp) -> composeUp.finalizedBy(composeDownTaskName)); + startTask.configure((start) -> { + start.dependsOn(composeUpTaskName); + start.getInternalEnvironment().putAll(environment(project, composeSettings)); + }); + appTestTask + .configure((appTest) -> appTest.getInternalEnvironment().putAll(environment(project, composeSettings))); + project.getTasks().named(composeDownTaskName).configure((composeDown) -> composeDown.mustRunAfter(stopTask)); + } + + private Provider> environment(Project project, ComposeSettings settings) { + return project.provider(() -> { + Map environment = new HashMap<>(); + settings.getServicesInfos().forEach((serviceName, service) -> { + String name = serviceName.toUpperCase(Locale.ENGLISH); + environment.put(name + "_HOST", service.getHost()); + service.getTcpPorts() + .forEach((source, target) -> environment.put(name + "_PORT_" + source, Integer.toString(target))); + }); + return environment; + }); + } + + private TaskProvider createStopApplicationTask(Project project, ApplicationType type, + TaskProvider startTask) { + String taskName = switch (type) { + case JVM -> "stopApp"; + }; + TaskProvider stopTask = project.getTasks() + .register(taskName, StopApplication.class, (stop) -> { + stop.getPidFile().set(startTask.flatMap(StartApplication::getPidFile)); + stop.setDescription("Stops the " + type.description + " application."); + }); + startTask.configure((start) -> start.finalizedBy(stopTask)); + return stopTask; + } + + private TaskProvider createStartApplicationTask(Project project, ApplicationType type, + Provider applicationBinary, Provider outputDirectory, + CrSmokeTestExtension extension) { + String taskName = switch (type) { + case JVM -> "startApp"; + }; + return project.getTasks().register(taskName, type.startTaskType, (start) -> { + start.getApplicationBinary().set(applicationBinary); + start.getOutputDirectory().set(outputDirectory); + start.setDescription("Starts the " + type.description + " application."); + start.getWebApplication().convention(extension.getWebApplication()); + }); + } + + private TaskProvider createAppTestTask(Project project, SourceSet source, ApplicationType type, + TaskProvider startTask, TaskProvider stopTask) { + String taskName = switch (type) { + case JVM -> "appTest"; + }; + TaskProvider appTestTask = project.getTasks().register(taskName, AppTest.class, (task) -> { + task.dependsOn(startTask); + task.useJUnitPlatform(); + task.setTestClassesDirs(source.getOutput().getClassesDirs()); + task.setClasspath(source.getRuntimeClasspath()); + task.getInputs() + .file(startTask.flatMap(StartApplication::getApplicationBinary)) + .withPropertyName("applicationBinary"); + task.systemProperty("org.springframework.cr.smoketest.standard-output", + startTask.get().getOutputFile().get().getAsFile().getAbsolutePath()); + task.finalizedBy(stopTask); + task.setDescription("Runs the app test suite against the " + type.description + " application."); + task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); + task.dependsOn(startTask); + }); + stopTask.configure((stop) -> stop.mustRunAfter(appTestTask)); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME).configure((check) -> check.dependsOn(appTestTask)); + return appTestTask; + } + + private enum ApplicationType { + + JVM("JVM", StartJvmApplication.class); + + private final String description; + + private final Class startTaskType; + + ApplicationType(String name, Class startTaskType) { + this.description = name; + this.startTaskType = startTaskType; + } + + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/dsl/CrSmokeTestExtension.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/dsl/CrSmokeTestExtension.java new file mode 100644 index 0000000..1d7fff6 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/dsl/CrSmokeTestExtension.java @@ -0,0 +1,47 @@ +/* + * Copyright 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.cr.gradle.dsl; + +import javax.inject.Inject; + +import org.gradle.api.Project; +import org.gradle.api.provider.Property; + +/** + * DSL extension for configuring checkpoint/restore smoke tests. + * + * @author Andy Wilkinson + * @author Sebastien Deleuze + */ +public class CrSmokeTestExtension { + + private final Property webApplication; + + @Inject + public CrSmokeTestExtension(Project project) { + this.webApplication = project.getObjects().property(Boolean.class); + } + + /** + * Whether the application under test is a web application. + * @return whether the application under test is a web application. + */ + public Property getWebApplication() { + return this.webApplication; + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/dsl/package-info.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/dsl/package-info.java new file mode 100644 index 0000000..b0bf4e0 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/dsl/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 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. + */ + +/** + * DSL for smoke testing of applications using checkpoint/restore. + */ +package org.springframework.cr.gradle.dsl; diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/package-info.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/package-info.java new file mode 100644 index 0000000..69b1060 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 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. + */ + +/** + * Gradle plugin for smoke testing of applications using checkpoint/restore. + */ +package org.springframework.cr.gradle; diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/AbstractSmokeTestsTask.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/AbstractSmokeTestsTask.java new file mode 100644 index 0000000..e229b1f --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/AbstractSmokeTestsTask.java @@ -0,0 +1,81 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputFiles; + +/** + * Base class for tasks that work with smoke tests. + * + * @author Andy Wilkinson + * @author Sebastien Deleuze + */ +public abstract class AbstractSmokeTestsTask extends DefaultTask { + + private FileCollection smokeTestDescriptions; + + @InputFiles + public FileCollection getSmokeTests() { + return this.smokeTestDescriptions; + } + + public void setSmokeTests(FileCollection smokeTests) { + this.smokeTestDescriptions = smokeTests; + } + + Map> smokeTests() { + List smokeTests = this.smokeTestDescriptions.getFiles() + .stream() + .map(this::readProperties) + .map(SmokeTest::new) + .collect(Collectors.toList()); + Map> groupedSmokeTests = new TreeMap<>(); + for (SmokeTest smokeTest : smokeTests) { + groupedSmokeTests + .computeIfAbsent(smokeTest.group(), + (group) -> new TreeSet<>((one, two) -> one.name().compareTo(two.name()))) + .add(smokeTest); + } + return groupedSmokeTests; + } + + private Properties readProperties(File smokeTest) { + Properties properties = new Properties(); + try (InputStream input = new FileInputStream(new File(smokeTest, "smoke-tests.properties"))) { + properties.load(input); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + return properties; + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/AppTest.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/AppTest.java new file mode 100644 index 0000000..2529e87 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/AppTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import org.gradle.api.provider.MapProperty; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.testing.Test; + +/** + * Custom {@link Test} task for app tests. + * + * @author Moritz Halbritter + */ +public abstract class AppTest extends Test { + + @Internal + public abstract MapProperty getInternalEnvironment(); + + @Override + public void executeTests() { + environment(getInternalEnvironment().get()); + super.executeTests(); + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/DescribeSmokeTests.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/DescribeSmokeTests.java new file mode 100644 index 0000000..d5c76cd --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/DescribeSmokeTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; + +/** + * {@link Task} that describes a project's smoke tests. + * + * @author Andy Wilkinson + */ +public abstract class DescribeSmokeTests extends DefaultTask { + + private FileCollection appTests; + + private FileCollection tests; + + public DescribeSmokeTests() { + getSmokeTestsDescription().put("appTests", + getProject().provider(() -> Boolean.toString(!this.appTests.isEmpty()))); + getSmokeTestsDescription().put("tests", getProject().provider(() -> Boolean.toString(!this.tests.isEmpty()))); + getSmokeTestsDescription().put("path", getProject().provider(getProject()::getPath)); + getSmokeTestsDescription().put("group", getProject().provider(getProject().getParent()::getName)); + getSmokeTestsDescription().put("name", getProject().provider(getProject()::getName)); + } + + @Input + abstract MapProperty getSmokeTestsDescription(); + + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + @InputFiles + public FileCollection getAppTests() { + return this.appTests; + } + + public void setAppTests(FileCollection appTests) { + this.appTests = appTests; + } + + @InputFiles + public FileCollection getTests() { + return this.tests; + } + + public void setTests(FileCollection tests) { + this.tests = tests; + } + + @TaskAction + void describeSmokeTests() throws IOException { + File smokeTests = getOutputDirectory().file("smoke-tests.properties").get().getAsFile(); + List lines = getSmokeTestsDescription().get() + .entrySet() + .stream() + .map((entry) -> entry.getKey() + "=" + entry.getValue()) + .sorted() + .toList(); + Files.write(smokeTests.toPath(), lines); + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/SmokeTest.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/SmokeTest.java new file mode 100644 index 0000000..31a60ae --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/SmokeTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.util.Properties; + +/** + * A smoke test. + * + * @author Andy Wilkinson + * @param name name of the smoke test + * @param group group of the smoke test + * @param path path of the smoke test project + * @param tests whether the smoke test contains any unit tests + * @param appTests whether the smoke test contains any app tests + */ +record SmokeTest(String name, String group, String path, boolean tests, boolean appTests) { + + SmokeTest(Properties properties) { + this(properties.getProperty("name"), properties.getProperty("group"), properties.getProperty("path"), + Boolean.valueOf(properties.getProperty("tests")), Boolean.valueOf(properties.getProperty("appTests"))); + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StartApplication.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StartApplication.java new file mode 100644 index 0000000..f105d32 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StartApplication.java @@ -0,0 +1,114 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.TaskAction; + +/** + * Base class for a {@link Task} that starts and application. + * + * @author Andy WilkinsonO + */ +public abstract class StartApplication extends DefaultTask { + + public StartApplication() { + getPidFile().convention(getOutputDirectory().map((dir) -> dir.file("pid"))); + getOutputFile().convention(getOutputDirectory().map((dir) -> dir.file("output.txt"))); + getErrorFile().convention(getOutputDirectory().map((dir) -> dir.file("error.txt"))); + } + + /** + * The application's binary. + * @return the application's binary + */ + @InputFile + public abstract RegularFileProperty getApplicationBinary(); + + /** + * Whether the application to be started is a web application. When it is, the + * application is started with {@code -Dserver.port=0} to prevent port clashes. + * @return whether the application is a web application + */ + @Input + public abstract Property getWebApplication(); + + /** + * The directory to which output should be written. Controls the location of the pid, + * output, and error files. + * @return the output directory + */ + @Internal + public abstract DirectoryProperty getOutputDirectory(); + + /** + * The file to which the application's pid should be written. + * @return the pid file + */ + @Internal + public abstract RegularFileProperty getPidFile(); + + /** + * The file to which the application's redirected output stream should be written. + * @return the output file + */ + @Internal + public abstract RegularFileProperty getOutputFile(); + + /** + * The file to which the application's redirected error stream should be written. + * @return the error file + */ + @Internal + public abstract RegularFileProperty getErrorFile(); + + @Internal + public abstract MapProperty getInternalEnvironment(); + + @TaskAction + void startApplication() throws IOException { + getOutputDirectory().getAsFile().get().mkdirs(); + File redirectedError = getErrorFile().get().getAsFile(); + File redirectedOutput = getOutputFile().get().getAsFile(); + Path pid = getPidFile().get().getAsFile().toPath(); + Files.deleteIfExists(redirectedError.toPath()); + Files.deleteIfExists(redirectedOutput.toPath()); + Files.deleteIfExists(pid); + ProcessBuilder processBuilder = prepareProcessBuilder(new ProcessBuilder()).redirectError(redirectedError) + .redirectOutput(redirectedOutput); + processBuilder.environment().putAll(getInternalEnvironment().get()); + Process process = processBuilder.start(); + Files.write(pid, List.of(Long.toString(process.pid()))); + } + + protected abstract ProcessBuilder prepareProcessBuilder(ProcessBuilder processBuilder); + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StartJvmApplication.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StartJvmApplication.java new file mode 100644 index 0000000..c457296 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StartJvmApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.gradle.api.Task; +import org.gradle.internal.jvm.Jvm; + +/** + * {@link Task} to start an application on the JVM. + * + * @author Andy Wilkinson + */ +public abstract class StartJvmApplication extends StartApplication { + + @Override + protected ProcessBuilder prepareProcessBuilder(ProcessBuilder processBuilder) { + File executable = Jvm.current().getJavaExecutable(); + List command = new ArrayList<>(); + command.add(executable.getAbsolutePath()); + if (getWebApplication().get()) { + command.add("-Dserver.port=0"); + } + command.add("-jar"); + command.add(getApplicationBinary().get().getAsFile().getAbsolutePath()); + return processBuilder.command(command); + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StopApplication.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StopApplication.java new file mode 100644 index 0000000..824e94c --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/StopApplication.java @@ -0,0 +1,66 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.TaskAction; + +/** + * {@link Task} to stop a {@link StartApplication} previously started application. + * + * @author Andy Wilkinson + */ +public class StopApplication extends DefaultTask { + + private final RegularFileProperty pidFile; + + public StopApplication() { + this.pidFile = getProject().getObjects().fileProperty(); + } + + /** + * The file to which the application's pid has been written. + * @return the pid file + */ + @InputFile + public RegularFileProperty getPidFile() { + return this.pidFile; + } + + @TaskAction + void stopJvmApp() throws IOException { + Path pidPath = this.pidFile.get().getAsFile().toPath(); + List lines = Files.readAllLines(pidPath); + if (lines.size() == 1) { + long pid = Long.parseLong(lines.get(0)); + ProcessHandle.of(pid).ifPresent((process) -> process.destroy()); + Files.delete(pidPath); + } + else { + System.out.println("No PID. Assuming process is stopped"); + } + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/UpdateConcoursePipeline.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/UpdateConcoursePipeline.java new file mode 100644 index 0000000..3a364d0 --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/UpdateConcoursePipeline.java @@ -0,0 +1,58 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** + * Task to update the Concourse pipeline for the smoke tests. + * + * @author Andy Wilkinson + */ +public abstract class UpdateConcoursePipeline extends AbstractSmokeTestsTask { + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + void updatePipeline() { + File outputFile = getOutputFile().getAsFile().get(); + try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) { + writer.println("groups:"); + smokeTests().forEach((group, tests) -> { + writer.println(" - name: " + group); + writer.println(" smoke_tests:"); + tests.forEach((smokeTest) -> { + writer.println(" - name: " + smokeTest.name()); + writer.println(" app_test: " + smokeTest.appTests()); + writer.println(" test: " + smokeTest.tests()); + }); + }); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/UpdateStatusPage.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/UpdateStatusPage.java new file mode 100644 index 0000000..dcf17fb --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/UpdateStatusPage.java @@ -0,0 +1,119 @@ +/* + * Copyright 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.cr.gradle.tasks; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** + * Task to update an Asciidoctor status page for the smoke tests. + * + * @author Andy Wilkinson + * @author Sebastien Deleuze + */ +public abstract class UpdateStatusPage extends AbstractSmokeTestsTask { + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + void updateStatusPage() throws IOException { + List lines = new ArrayList<>(); + lines.add("= Smoke Tests Status"); + lines.add(":toc:"); + lines.add(":toc-title: Projects"); + lines.add(""); + smokeTests().forEach((group, tests) -> { + lines.add("== " + capitalize(group)); + lines.add(""); + lines.add("[%header,cols=\"" + (TestType.values().length + 1) + "\"]"); + lines.add("|==="); + lines.add("h|Smoke Test"); + for (TestType testType : TestType.values()) { + lines.add("h|" + testType.taskName()); + } + lines.add(""); + for (SmokeTest test : tests) { + lines.add("|" + test.name()); + for (TestType testType : TestType.values()) { + lines.add("|" + testType.badge(test)); + } + lines.add(""); + } + lines.add("|==="); + lines.add(""); + }); + Files.write(getOutputFile().get().getAsFile().toPath(), lines); + } + + private String capitalize(String input) { + StringBuffer buffer = new StringBuffer(input.length()); + for (char c : input.toCharArray()) { + buffer.append(buffer.isEmpty() ? Character.toUpperCase(c) : c); + } + return buffer.toString(); + } + + private enum TestType { + + APP_TEST(SmokeTest::appTests, "-app-test", "appTest"), + TEST(SmokeTest::tests, "-test", "test"); + + private final Predicate predicate; + + private final String urlSuffix; + + private final String taskName; + + TestType(Predicate predicate, String suffix, String taskName) { + this.predicate = predicate; + this.urlSuffix = suffix; + this.taskName = taskName; + } + + String badge(SmokeTest smokeTest) { + if (!this.predicate.test(smokeTest)) { + return ""; + } + return "image:" + badgeUrl(smokeTest.name(), this.urlSuffix) + "[link=" + + jobUrl(smokeTest.name(), this.urlSuffix) + "]"; + } + + String taskName() { + return this.taskName; + } + + private String badgeUrl(String name, String suffix) { + return "https://ci.spring.io/api/v1/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/" + + name + suffix + "/badge"; + } + + private String jobUrl(String name, String suffix) { + return "https://ci.spring.io/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/" + + name + suffix; + } + + } + +} diff --git a/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/package-info.java b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/package-info.java new file mode 100644 index 0000000..b5a811b --- /dev/null +++ b/gradle/plugins/cr-smoke-test-plugin/src/main/java/org/springframework/cr/gradle/tasks/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 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. + */ + +/** + * Tasks for smoke testing of applications using checkpoint/restore. + */ +package org.springframework.cr.gradle.tasks; diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..943f0cb Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f398c33 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..65dcd68 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..8e9ed8e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,33 @@ +pluginManagement { + includeBuild "gradle/plugins/cr-smoke-test-plugin" + repositories { + gradlePluginPortal() + maven { + url "https://repo.spring.io/snapshot" + } + maven { + url 'https://repo.spring.io/milestone' + } + maven { + url "https://repo.spring.io/release" + content { + includeGroup "io.spring.ge.conventions" + } + } + } +} + +plugins { + id "com.gradle.enterprise" version "3.12.6" + id "io.spring.ge.conventions" version "0.0.13" +} + +rootProject.name="spring-checkpoint-restore-smoke-tests" + +include "cr-smoke-test-support" + +["framework"].each { group -> + file(group).eachDirMatch(~/[a-z].*/) { smokeTest -> + include "$group:${smokeTest.name}" + } +}