diff --git a/release-scripts/pom.xml b/release-scripts/pom.xml index df7eb42ae8d5..577a6894f9b8 100644 --- a/release-scripts/pom.xml +++ b/release-scripts/pom.xml @@ -83,5 +83,15 @@ log4j-slf4j-impl runtime + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + diff --git a/release-scripts/src/main/java/software/amazon/awssdk/release/UpdateServiceMain.java b/release-scripts/src/main/java/software/amazon/awssdk/release/UpdateServiceMain.java index cc55cd880f92..485d14b4c6df 100644 --- a/release-scripts/src/main/java/software/amazon/awssdk/release/UpdateServiceMain.java +++ b/release-scripts/src/main/java/software/amazon/awssdk/release/UpdateServiceMain.java @@ -36,7 +36,8 @@ --service-module-name service-module-name --service-json /path/to/service-2.json [--paginators-json /path/to/paginators-1.json - --waiters-json /path/to/waiters-2.json]" + --waiters-json /path/to/waiters-2.json + --endpoint-bdd-json /path/to/endpoint-bdd-1.json]" * */ public class UpdateServiceMain extends Cli { @@ -50,7 +51,10 @@ private UpdateServiceMain() { optionalOption("paginators-json", "The paginators-1.json file for the service."), optionalOption("waiters-json", "The waiters-2.json file for the service."), optionalOption("endpoint-rule-set-json", "The endpoint-rule-set.json file for the service."), - optionalOption("endpoint-tests-json", "The endpoint-tests.json file for the service.")); + optionalOption("endpoint-tests-json", "The endpoint-tests.json file for the service."), + optionalOption("endpoint-bdd-json", "The endpoint-bdd-1.json file for the service. Only copied for a " + + "service that already has one, so that BDD endpoint resolution " + + "stays opt-in per service.")); } public static void main(String[] args) { @@ -71,6 +75,7 @@ private static class ServiceUpdater { private final Path waitersJson; private final Path endpointRuleSetJson; private final Path endpointTestsJson; + private final Path endpointBddJson; private ServiceUpdater(CommandLine commandLine) { this.mavenProjectRoot = Paths.get(commandLine.getOptionValue("maven-project-root").trim()); @@ -81,6 +86,7 @@ private ServiceUpdater(CommandLine commandLine) { this.waitersJson = optionalPath(commandLine.getOptionValue("waiters-json")); this.endpointRuleSetJson = optionalPath(commandLine.getOptionValue("endpoint-rule-set-json")); this.endpointTestsJson = optionalPath(commandLine.getOptionValue("endpoint-tests-json")); + this.endpointBddJson = optionalPath(commandLine.getOptionValue("endpoint-bdd-json")); } private Path optionalPath(String path) { @@ -101,6 +107,7 @@ public void run() throws Exception { copyFile(waitersJson, codegenFileLocation.resolve("waiters-2.json")); copyFile(endpointRuleSetJson, codegenFileLocation.resolve("endpoint-rule-set.json")); copyFile(endpointTestsJson, codegenFileLocation.resolve("endpoint-tests.json")); + copyFileIfAlreadyPresent(endpointBddJson, codegenFileLocation.resolve("endpoint-bdd-1.json")); } private Path codegenFileLocation(String serviceModuleName, String serviceId) { @@ -132,5 +139,22 @@ private void copyFile(Path source, Path destination) throws IOException { FileUtils.copyFile(source.toFile(), destination.toFile()); } } + + /** + * Updates {@code destination} from {@code source} only when {@code destination} already exists, leaving a + * service that does not have the file without one. + */ + private void copyFileIfAlreadyPresent(Path source, Path destination) throws IOException { + if (source == null || !Files.isRegularFile(source)) { + return; + } + if (!Files.isRegularFile(destination)) { + log.info(() -> "Skipping " + source + " because " + destination + " does not exist. Add the file to opt " + + "this service in."); + return; + } + log.info(() -> "Copying " + source + " to " + destination); + FileUtils.copyFile(source.toFile(), destination.toFile()); + } } } diff --git a/release-scripts/src/test/java/software/amazon/awssdk/release/UpdateServiceMainTest.java b/release-scripts/src/test/java/software/amazon/awssdk/release/UpdateServiceMainTest.java new file mode 100644 index 000000000000..c6dae04432cc --- /dev/null +++ b/release-scripts/src/test/java/software/amazon/awssdk/release/UpdateServiceMainTest.java @@ -0,0 +1,220 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.release; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Covers how {@link UpdateServiceMain} copies model files into a service's {@code codegen-resources} directory, and in + * particular the gate on {@code endpoint-bdd-1.json}. + * + *

Whether a service resolves endpoints through the BDD provider is decided by whether {@code endpoint-bdd-1.json} is + * checked in. Upstream publishes the model for every service that has one, so copying it unconditionally would move + * every service onto the BDD provider on the first release that carried it. Adding the file by hand is the act of + * opting a service in; the release script only keeps an existing one up to date. + * + *

Driven through {@code main} rather than by calling the copy helper directly, so the option registration and the + * destination path are covered too. An unregistered option would leave the argument silently ignored, which no test of + * the helper alone would catch. + */ +class UpdateServiceMainTest { + + @Test + void endpointBddJson_notAlreadyPresent_isNotCopied(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp); + + fixture.run(); + + assertThat(fixture.destination("endpoint-bdd-1.json")) + .as("a service without a checked-in BDD model must not be opted in by a release") + .doesNotExist(); + } + + @Test + void endpointBddJson_alreadyPresent_isOverwritten(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp); + fixture.writeDestination("endpoint-bdd-1.json", "{\"stale\": true}"); + + fixture.run(); + + assertThat(fixture.destination("endpoint-bdd-1.json")).hasContent(Fixture.BDD_CONTENT); + } + + /** + * An empty file still counts as present. It is a checked-in file, so the service is opted in, and refusing to + * refresh it would leave it stale forever. + */ + @Test + void endpointBddJson_presentButEmpty_isStillOverwritten(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp); + fixture.writeDestination("endpoint-bdd-1.json", ""); + + fixture.run(); + + assertThat(fixture.destination("endpoint-bdd-1.json")).hasContent(Fixture.BDD_CONTENT); + } + + /** + * The gate is on the destination, not the source, so omitting the argument leaves an opted-in service's model + * alone rather than deleting or emptying it. + */ + @Test + void endpointBddJson_argumentOmitted_leavesAnExistingModelAlone(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp); + fixture.writeDestination("endpoint-bdd-1.json", "{\"kept\": true}"); + + fixture.withoutEndpointBddArgument().run(); + + assertThat(fixture.destination("endpoint-bdd-1.json")).hasContent("{\"kept\": true}"); + } + + @Test + void endpointBddJson_isOptionalAndAbsenceIsNotAnError(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp); + + fixture.withoutEndpointBddArgument().run(); + + assertThat(fixture.destination("service-2.json")).hasContent(Fixture.SERVICE_CONTENT); + assertThat(fixture.destination("endpoint-bdd-1.json")).doesNotExist(); + } + + /** + * The other model files keep their unconditional behaviour. A service legitimately gains waiters or endpoint tests + * for the first time, and those must land on the release that carries them. + */ + @Test + void otherModelFiles_areCopiedEvenWhenNotAlreadyPresent(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp); + + fixture.run(); + + assertThat(fixture.destination("service-2.json")).hasContent(Fixture.SERVICE_CONTENT); + assertThat(fixture.destination("paginators-1.json")).exists(); + assertThat(fixture.destination("waiters-2.json")).exists(); + assertThat(fixture.destination("endpoint-rule-set.json")).exists(); + assertThat(fixture.destination("endpoint-tests.json")).exists(); + } + + /** + * DynamoDB's models live in a nested directory, so this checks the gate is applied against the same resolved + * destination the copy uses rather than the module root. + */ + @Test + void endpointBddJson_gateAppliesToTheResolvedNestedDestination(@TempDir Path tmp) throws Exception { + Fixture fixture = new Fixture(tmp, "dynamodb", "DynamoDB", "dynamodb"); + fixture.writeDestination("endpoint-bdd-1.json", "{\"stale\": true}"); + + fixture.run(); + + assertThat(fixture.destination("endpoint-bdd-1.json")).hasContent(Fixture.BDD_CONTENT); + assertThat(tmp.resolve("services/dynamodb/src/main/resources/codegen-resources/endpoint-bdd-1.json")) + .as("the nested service must not also get a copy at the module root") + .doesNotExist(); + } + + private static final class Fixture { + private static final String BDD_CONTENT = "{\"version\": \"1.0\", \"nodes\": \"AAAA\"}"; + private static final String SERVICE_CONTENT = "{\"metadata\": {}}"; + + private final Path projectRoot; + private final Path sourceDir; + private final Path destinationDir; + private final String serviceModuleName; + private final String serviceId; + private boolean includeEndpointBddArgument = true; + + private Fixture(Path tmp) throws IOException { + this(tmp, "myservice", "MyService", null); + } + + private Fixture(Path tmp, String serviceModuleName, String serviceId, String nestedDir) throws IOException { + this.projectRoot = tmp.resolve("project"); + this.sourceDir = tmp.resolve("source"); + this.serviceModuleName = serviceModuleName; + this.serviceId = serviceId; + + Path codegenResources = projectRoot.resolve("services") + .resolve(serviceModuleName) + .resolve("src") + .resolve("main") + .resolve("resources") + .resolve("codegen-resources"); + this.destinationDir = nestedDir == null ? codegenResources : codegenResources.resolve(nestedDir); + Files.createDirectories(destinationDir); + Files.createDirectories(sourceDir); + + write(sourceDir.resolve("service-2.json"), SERVICE_CONTENT); + write(sourceDir.resolve("paginators-1.json"), "{\"pagination\": {}}"); + write(sourceDir.resolve("waiters-2.json"), "{\"waiters\": {}}"); + write(sourceDir.resolve("endpoint-rule-set.json"), "{\"version\": \"1.0\"}"); + write(sourceDir.resolve("endpoint-tests.json"), "{\"testCases\": []}"); + write(sourceDir.resolve("endpoint-bdd-1.json"), BDD_CONTENT); + } + + private Fixture withoutEndpointBddArgument() { + this.includeEndpointBddArgument = false; + return this; + } + + private void writeDestination(String name, String content) throws IOException { + write(destinationDir.resolve(name), content); + } + + private Path destination(String name) { + return destinationDir.resolve(name); + } + + private void run() { + List args = new ArrayList<>(); + args.add("--maven-project-root"); + args.add(projectRoot.toString()); + args.add("--service-module-name"); + args.add(serviceModuleName); + args.add("--service-id"); + args.add(serviceId); + args.add("--service-json"); + args.add(sourceDir.resolve("service-2.json").toString()); + args.add("--paginators-json"); + args.add(sourceDir.resolve("paginators-1.json").toString()); + args.add("--waiters-json"); + args.add(sourceDir.resolve("waiters-2.json").toString()); + args.add("--endpoint-rule-set-json"); + args.add(sourceDir.resolve("endpoint-rule-set.json").toString()); + args.add("--endpoint-tests-json"); + args.add(sourceDir.resolve("endpoint-tests.json").toString()); + if (includeEndpointBddArgument) { + args.add("--endpoint-bdd-json"); + args.add(sourceDir.resolve("endpoint-bdd-1.json").toString()); + } + + UpdateServiceMain.main(args.toArray(new String[0])); + } + + private static void write(Path path, String content) throws IOException { + Files.createDirectories(path.getParent()); + Files.write(path, content.getBytes(UTF_8)); + } + } +}