diff --git a/docs/src/main/asciidoc/verifier_contract.adoc b/docs/src/main/asciidoc/verifier_contract.adoc index ee2eaa7fb2..f127194d43 100644 --- a/docs/src/main/asciidoc/verifier_contract.adoc +++ b/docs/src/main/asciidoc/verifier_contract.adoc @@ -1820,7 +1820,7 @@ own implementation of the `StubGenerator` interface. The following code listing [source,groovy] ---- -include::{converters_path}/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.groovy[indent=0,lines=16..-1] +include::{converters_path}/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.java[indent=0,lines=16..-1] ---- Again, you must provide a `spring.factories` file, such as the one shown in the following diff --git a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/GoodbyeWorldTests.java b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/GoodbyeWorldTests.java new file mode 100644 index 0000000000..651f5e0e70 --- /dev/null +++ b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/GoodbyeWorldTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2019 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.loan; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerPort; +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::autoconfigure_stubrunner[] +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = { + "com.example:http-server-dsl"}, + repositoryRoot = "stubs://classpath://contractsAtRuntime/", + stubsMode = StubRunnerProperties.StubsMode.LOCAL, + generateStubs = true) +public class GoodbyeWorldTests { + + // end::autoconfigure_stubrunner[] + + @StubRunnerPort("http-server-dsl") + int port; + + @Test + public void shouldGenerateStubsAtRuntime() { + // when: + String response = new RestTemplate().getForObject("http://localhost:" + this.port + "/goodbye", String.class); + // then: + assertThat(response) + .isEqualTo("Goodbye World!"); + } + +} diff --git a/samples/standalone/dsl/http-client/src/test/resources/contractsAtRuntime/goodbyeWorld.groovy b/samples/standalone/dsl/http-client/src/test/resources/contractsAtRuntime/goodbyeWorld.groovy new file mode 100644 index 0000000000..61e37c5f8e --- /dev/null +++ b/samples/standalone/dsl/http-client/src/test/resources/contractsAtRuntime/goodbyeWorld.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2019 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. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + url("/goodbye") + method(GET()) + } + response { + status(OK()) + body("Goodbye World!") + } +} diff --git a/spring-cloud-contract-stub-runner/pom.xml b/spring-cloud-contract-stub-runner/pom.xml index aa44689adc..f98d0d23ef 100644 --- a/spring-cloud-contract-stub-runner/pom.xml +++ b/spring-cloud-contract-stub-runner/pom.xml @@ -22,6 +22,10 @@ org.springframework.cloud spring-cloud-contract-shade + + org.springframework.cloud + spring-cloud-contract-converters + org.springframework.cloud spring-cloud-stream-test-support diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java new file mode 100644 index 0000000000..4e42baaf02 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2019 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.cloud.contract.stubrunner; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.verifier.converter.StubGenerator; +import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider; +import org.springframework.cloud.contract.verifier.file.ContractMetadata; +import org.springframework.util.StringUtils; + +final class MappingGenerator { + + private static final Log log = LogFactory.getLog(MappingGenerator.class); + + private MappingGenerator() { + throw new IllegalStateException("Can't instantiate utility class"); + } + + static Collection toMappings(File contractFile, Collection contracts, + File mappingsFolder) { + StubGeneratorProvider provider = new StubGeneratorProvider(); + Collection stubGenerators = provider + .converterForName(contractFile.getName()); + if (log.isDebugEnabled()) { + log.debug("Found following matching stub generators " + stubGenerators); + } + Collection mappings = new LinkedList<>(); + for (StubGenerator stubGenerator : stubGenerators) { + Map map = stubGenerator.convertContents( + contractFile.getName(), new ContractMetadata(contractFile.toPath(), + false, contracts.size(), null, contracts)); + for (Map.Entry entry : map.entrySet()) { + String value = entry.getValue(); + File mapping = new File(mappingsFolder, + StringUtils.stripFilenameExtension(contractFile.getName()) + "_" + + Math.abs(entry.getKey().hashCode()) + + stubGenerator.fileExtension()); + mappings.add(storeFile(mapping.toPath(), value.getBytes())); + } + } + return mappings; + } + + private static Path storeFile(Path path, byte[] contents) { + try { + Path storedPath = Files.write(path, contents); + if (log.isDebugEnabled()) { + log.debug("Stored file [" + path.toString() + "]"); + } + return storedPath; + } + catch (IOException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java index 8cd0c48751..69a17ebc7a 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java @@ -17,7 +17,13 @@ package org.springframework.cloud.contract.stubrunner; import java.io.File; -import java.lang.invoke.MethodHandles; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @@ -25,7 +31,12 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties; +import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter; +import org.springframework.cloud.contract.verifier.converter.StubGenerator; +import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider; import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; +import org.springframework.core.io.Resource; /** * Factory of StubRunners. Basing on the options and passed collaborators downloads the @@ -33,8 +44,7 @@ import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; */ class StubRunnerFactory { - private static final Log log = LogFactory - .getLog(MethodHandles.lookup().lookupClass()); + private static final Log log = LogFactory.getLog(StubRunnerFactory.class); private final StubRunnerOptions stubRunnerOptions; @@ -69,13 +79,93 @@ class StubRunnerFactory { + "] the downloaded entry is [" + entry + "]"); } if (entry != null) { - result.add(createStubRunner(entry.getKey(), entry.getValue())); + Path path = resolvePath(entry.getValue()); + File unpackedLocation = path.toFile(); + if (this.stubRunnerOptions.isGenerateStubs()) { + if (log.isDebugEnabled()) { + log.debug( + "Flag to generate stubs at runtime was switched on. Will remove the current mappings and will generate new ones."); + } + generateMappingsAtRuntime(path); + } + result.add(createStubRunner(entry.getKey(), unpackedLocation)); } - } return result; } + private void generateMappingsAtRuntime(Path path) { + removeCurrentMappings(path); + generateNewMappings(path); + } + + private Path resolvePath(File unpackedLocation) { + Resource resource = ResourceResolver.resource(unpackedLocation.getPath()); + Path path = unpackedLocation.toPath(); + if (resource != null) { + try { + return Paths.get(resource.getURI()); + } + catch (IOException ex) { + return unpackedLocation.toPath(); + } + } + return path; + } + + private void removeCurrentMappings(Path path) { + try { + Files.walkFileTree(path, new SimpleFileVisitor() { + + private final Log log = LogFactory.getLog(StubRunnerFactory.class); + + private final StubGeneratorProvider provider = new StubGeneratorProvider(); + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + Collection stubGenerators = this.provider + .converterForName(file.toString()); + if (!stubGenerators.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug("Deleting file [" + file.toString() + + "] since at least one stub generator would run it"); + } + try { + Files.delete(file); + } + catch (IOException ex) { + log.warn("Failed to delete file [" + file.toString() + "]", + ex); + } + } + return FileVisitResult.CONTINUE; + } + }); + } + catch (IOException ex) { + log.warn("Exception occurred while trying to delete mappings", ex); + } + } + + private void generateNewMappings(Path path) { + File unpackedLocation = path.toFile(); + ContractVerifierConfigProperties configProperties = new ContractVerifierConfigProperties(); + configProperties + .setContractsDslDir(subfolderIfPresent(unpackedLocation, "contracts")); + configProperties + .setStubsOutputDir(subfolderIfPresent(unpackedLocation, "mappings")); + RecursiveFilesConverter converter = new RecursiveFilesConverter(configProperties); + converter.processFiles(); + } + + private File subfolderIfPresent(File unpackedLocation, String subfolder) { + File subfolderDir = new File(unpackedLocation, subfolder); + if (subfolderDir.exists()) { + return subfolderDir; + } + return unpackedLocation; + } + private StubRunner createStubRunner(StubConfiguration stubsConfiguration, File unzipedStubDir) { if (unzipedStubDir == null) { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.java index 6e029be3c3..233fab56fa 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptions.java @@ -116,6 +116,12 @@ public class StubRunnerOptions { */ private boolean deleteStubsAfterTest; + /** + * When enabled, this flag will tell stub runner to not load the generated stubs, but + * convert the found contracts at runtime to a stub format and run those stubs. + */ + private boolean generateStubs; + /** * Map of properties that can be passed to custom * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}. @@ -128,7 +134,8 @@ public class StubRunnerOptions { Map stubIdsToPortMapping, String username, String password, final StubRunnerProxyOptions stubRunnerProxyOptions, boolean stubsPerConsumer, String consumerName, String mappingsOutputFolder, - boolean deleteStubsAfterTest, Map properties, + boolean deleteStubsAfterTest, boolean generateStubs, + Map properties, Class httpServerStubConfigurer) { this.minPortValue = minPortValue; this.maxPortValue = maxPortValue; @@ -145,6 +152,7 @@ public class StubRunnerOptions { this.consumerName = consumerName; this.mappingsOutputFolder = mappingsOutputFolder; this.deleteStubsAfterTest = deleteStubsAfterTest; + this.generateStubs = generateStubs; this.properties = properties; this.httpServerStubConfigurer = httpServerStubConfigurer; } @@ -169,6 +177,8 @@ public class StubRunnerOptions { System.getProperty("stubrunner.mappings-output-folder")) .withDeleteStubsAfterTest(Boolean.parseBoolean( System.getProperty("stubrunner.delete-stubs-after-test", "true"))) + .withGenerateStubs(Boolean.parseBoolean( + System.getProperty("stubrunner.generate-stubs", "false"))) .withProperties(stubRunnerProps()); builder = httpStubConfigurer(builder); String proxyHost = System.getProperty("stubrunner.proxy.host"); @@ -318,6 +328,10 @@ public class StubRunnerOptions { this.deleteStubsAfterTest = deleteStubsAfterTest; } + public boolean isGenerateStubs() { + return this.generateStubs; + } + public Map getProperties() { return this.properties; } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.java index 733ba66be5..5c09456185 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilder.java @@ -69,6 +69,8 @@ public class StubRunnerOptionsBuilder { private boolean deleteStubsAfterTest = true; + private boolean generateStubs; + private Map properties = new HashMap<>(); private Class httpServerStubConfigurer = HttpServerStubConfigurer.NoOpHttpServerStubConfigurer.class; @@ -194,6 +196,7 @@ public class StubRunnerOptionsBuilder { this.stubIdsToPortMapping = options.stubIdsToPortMapping != null ? options.stubIdsToPortMapping : new LinkedHashMap<>(); this.deleteStubsAfterTest = options.isDeleteStubsAfterTest(); + this.generateStubs = options.isGenerateStubs(); this.properties = options.getProperties(); this.httpServerStubConfigurer = options.getHttpServerStubConfigurer(); return this; @@ -211,6 +214,11 @@ public class StubRunnerOptionsBuilder { return this; } + public StubRunnerOptionsBuilder withGenerateStubs(boolean generateStubs) { + this.generateStubs = generateStubs; + return this; + } + public StubRunnerOptionsBuilder withProperties(Map properties) { this.properties = properties; return this; @@ -228,7 +236,7 @@ public class StubRunnerOptionsBuilder { buildDependencies(), this.stubIdsToPortMapping, this.username, this.password, this.stubRunnerProxyOptions, this.stubsPerConsumer, this.consumerName, this.mappingsOutputFolder, this.deleteStubsAfterTest, - this.properties, this.httpServerStubConfigurer); + this.generateStubs, this.properties, this.httpServerStubConfigurer); } private Collection buildDependencies() { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtension.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtension.java index a2f2616f07..8911323787 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtension.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtension.java @@ -252,6 +252,12 @@ public class StubRunnerExtension implements BeforeAllCallback, AfterAllCallback, return new PortStubRunnerExtension(this.delegate); } + @Override + public StubRunnerExtension withGenerateStubs(boolean generateStubs) { + builder().withGenerateStubs(generateStubs); + return new PortStubRunnerExtension(this.delegate); + } + @Override public StubRunnerExtension withProperties(Map properties) { builder().withProperties(properties); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtensionOptions.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtensionOptions.java index c4d6059cd7..b8dac75b39 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtensionOptions.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerExtensionOptions.java @@ -149,6 +149,13 @@ interface StubRunnerExtensionOptions { */ StubRunnerExtension withDeleteStubsAfterTest(boolean deleteStubsAfterTest); + /** + * @param generateStubs If set to {@code true} will NOT load generated stubs but will + * generate stubs from contract definitions at runtime. + * @return the rule + */ + StubRunnerExtension withGenerateStubs(boolean generateStubs); + /** * @param properties Map of properties that can be passed to custom * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java index cce7bf66ea..ba52f4f04d 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java @@ -190,6 +190,12 @@ public class StubRunnerRule implements TestRule, StubFinder, StubRunnerRuleOptio return this.delegate; } + @Override + public StubRunnerRule withGenerateStubs(boolean generateStubs) { + builder().withGenerateStubs(true); + return this.delegate; + } + @Override public StubRunnerRule withProperties(Map properties) { builder().withProperties(properties); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleOptions.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleOptions.java index 9fbf06aa54..510970f341 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleOptions.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleOptions.java @@ -145,6 +145,13 @@ interface StubRunnerRuleOptions { */ StubRunnerRule withDeleteStubsAfterTest(boolean deleteStubsAfterTest); + /** + * @param generateStubs If set to {@code true} will NOT load generated stubs but will + * generate stubs from contract definitions at runtime. + * @return the rule + */ + StubRunnerRule withGenerateStubs(boolean generateStubs); + /** * @param properties Map of properties that can be passed to custom * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/AutoConfigureStubRunner.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/AutoConfigureStubRunner.java index 77f5169859..679724f8ae 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/AutoConfigureStubRunner.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/AutoConfigureStubRunner.java @@ -137,6 +137,13 @@ public @interface AutoConfigureStubRunner { */ boolean deleteStubsAfterTest() default true; + /** + * @return When enabled, this flag will tell stub runner to not load the generated + * stubs, but convert the found contracts at runtime to a stub format and run those + * stubs. + */ + boolean generateStubs() default false; + /** * Configuration for an HTTP server stub. * @return class that allows to perform additional HTTP server stub configuration diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java index e3b2349bc7..4b89970fe6 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java @@ -101,6 +101,7 @@ public class StubRunnerConfiguration { .withConsumerName(consumerName()) .withMappingsOutputFolder(this.props.getMappingsOutputFolder()) .withDeleteStubsAfterTest(this.props.isDeleteStubsAfterTest()) + .withGenerateStubs(this.props.isGenerateStubs()) .withProperties(this.props.getProperties()) .withHttpServerStubConfigurer(this.props.getHttpServerStubConfigurer()); } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerProperties.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerProperties.java index e8e264d576..41bd683a3f 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerProperties.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerProperties.java @@ -30,7 +30,6 @@ import org.springframework.util.StringUtils; /** * @author Dave Syer - * */ @ConfigurationProperties("stubrunner") public class StubRunnerProperties { @@ -109,6 +108,12 @@ public class StubRunnerProperties { */ private boolean deleteStubsAfterTest = true; + /** + * When enabled, this flag will tell stub runner to not load the generated stubs, but + * convert the found contracts at runtime to a stub format and run those stubs. + */ + private boolean generateStubs; + /** * Map of properties that can be passed to custom * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}. @@ -247,6 +252,14 @@ public class StubRunnerProperties { } } + public boolean isGenerateStubs() { + return this.generateStubs; + } + + public void setGenerateStubs(boolean generateStubs) { + this.generateStubs = generateStubs; + } + public Class getHttpServerStubConfigurer() { return this.httpServerStubConfigurer; } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy index a5b637001d..4c13826727 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerOptionsBuilderSpec.groovy @@ -231,7 +231,7 @@ class StubRunnerOptionsBuilderSpec extends Specification { given: StubRunnerOptionsBuilder builder = builder.withOptions(new StubRunnerOptions(1, 2, new FileSystemResource("root"), StubRunnerProperties.StubsMode.LOCAL, "classifier", [new StubConfiguration("a:b:c")], [(new StubConfiguration("a:b:c")): 3], "foo", "bar", - new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, [foo: "bar"], Foo)) + new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, true, [foo: "bar"], Foo)) builder.withStubs("foo:bar:baz") when: StubRunnerOptions options = builder.build() @@ -251,6 +251,7 @@ class StubRunnerOptionsBuilderSpec extends Specification { options.consumerName == "consumer" options.mappingsOutputFolder == "folder" options.deleteStubsAfterTest == false + options.generateStubs == true options.properties == [foo: "bar"] options.httpServerStubConfigurer == Foo } @@ -260,7 +261,7 @@ class StubRunnerOptionsBuilderSpec extends Specification { StubRunnerOptionsBuilder builder = builder.withOptions(new StubRunnerOptions(1, 2, new FileSystemResource("root"), StubRunnerProperties.StubsMode.CLASSPATH, "classifier", [new StubConfiguration("a:b:c")], [(new StubConfiguration("a:b:c")): 3], "username123", "password123", - new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, [:], Foo)) + new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, true, [:], Foo)) builder.withStubs("foo:bar:baz") when: String options = builder.build().toString() @@ -290,6 +291,8 @@ class StubRunnerOptionsBuilderSpec extends Specification { System.setProperty("stubrunner.properties.foo-bar", "bar") System.setProperty("stubrunner.properties.foo-baz", "baz") System.setProperty("stubrunner.properties.bar.bar", "foo") + System.setProperty("stubrunner.delete-stubs-after-test", "false") + System.setProperty("stubrunner.generate-stubs", "true") System.setProperty("stubrunner.http-server-stub-configurer", "org.springframework.cloud.contract.stubrunner.Foo") when: StubRunnerOptions options = StubRunnerOptions.fromSystemProps() @@ -305,6 +308,8 @@ class StubRunnerOptionsBuilderSpec extends Specification { options.proxyOptions.proxyHost == "host" options.proxyOptions.proxyPort == 4 options.stubsPerConsumer == true + options.deleteStubsAfterTest == false + options.generateStubs == true options.consumerName == "consumer" options.mappingsOutputFolder == "folder" options.properties == ["foo-bar": "bar", "foo-baz": "baz", "bar.bar": "foo"] diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy index 4e26f0f34f..ee3a603a2e 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy @@ -16,14 +16,21 @@ package org.springframework.cloud.contract.stubrunner + import spock.lang.Specification +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages +import org.springframework.util.SocketUtils + class StubRunnerSpec extends Specification { - private static final int MIN_PORT = 8111 - private static final int MAX_PORT = 8111 + private static final int MIN_PORT = SocketUtils.findAvailableTcpPort() + private static final int MAX_PORT = MIN_PORT private static final URL EXPECTED_STUB_URL = new URL("http://localhost:$MIN_PORT") + private static final URL generateStubs = StubRunnerSpec.getResource('/generateStubs/') + def 'should provide stub URL for provided groupid and artifactId'() { given: Arguments args = argumentsWithProjectDefinition() @@ -50,10 +57,42 @@ class StubRunnerSpec extends Specification { runner.close() } + + def 'should generate stubs at runtime'() { + given: + Arguments args = argumentsWithGenerateStubs() + StubDownloader downloader = new FileStubDownloader().build(args.stubRunnerOptions); + StubRunner runner = new StubRunnerFactory(args.stubRunnerOptions, + downloader, new NoOpStubMessages()).createStubsFromServiceConfiguration().first() + when: + runner.runStubs() + then: + URL url = runner.findStubUrl("groupId2", "artifactId2") + new URL(url.toString() + "/goodbye").text + when: + new URL(url.toString() + "/hello").text + then: + thrown(FileNotFoundException) + cleanup: + runner.close() + } + Arguments argumentsWithProjectDefinition() { StubConfiguration stubConfiguration = new StubConfiguration("groupId", "artifactId", "classifier") StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder().withMinMaxPort(MIN_PORT, MAX_PORT).build() return new Arguments(stubRunnerOptions, 'src/test/resources/repository', stubConfiguration) } + Arguments argumentsWithGenerateStubs() { + StubConfiguration stubConfiguration = new StubConfiguration("groupId2", "artifactId2", "classifier2") + StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder() + .withMinMaxPort(MIN_PORT, MAX_PORT) + .withGenerateStubs(true) + .withStubs(stubConfiguration.toString()) + .withStubsMode(StubRunnerProperties.StubsMode.REMOTE) + .withStubRepositoryRoot("stubs://file://" + generateStubs.path) + .build() + return new Arguments(stubRunnerOptions, 'src/test/resources/generateStubs', stubConfiguration) + } + } diff --git a/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye.groovy b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye.groovy new file mode 100644 index 0000000000..61e37c5f8e --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2019 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. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + url("/goodbye") + method(GET()) + } + response { + status(OK()) + body("Goodbye World!") + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/mappings/hello.json b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/mappings/hello.json new file mode 100644 index 0000000000..13d165d71d --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/mappings/hello.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/hello" + }, + "response": { + "status": 200, + "body": "Hello world!", + "headers": { + "Content-Type": "text/plain" + } + } +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.groovy deleted file mode 100644 index 1ce31e2cb9..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2013-2019 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.cloud.contract.verifier.converter - -import groovy.transform.CompileStatic - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.verifier.file.ContractMetadata - -/** - * Converts contracts into their stub representation. - * - * @since 1.1.0 - */ -@CompileStatic -interface StubGenerator { - - /** - * @return {@code true} if the converter can handle the file to convert it into a stub. - */ - boolean canHandleFileName(String fileName) - - /** - * @return the collection of converted contracts into stubs. One contract can - * result in multiple stubs. - */ - Map convertContents(String rootName, ContractMetadata content) - - /** - * @return the name of the converted stub file. If you have multiple contracts - * in a single file then a prefix will be added to the generated file. If you - * provide the {@link Contract#name} field then that field will override the - * generated file name. - * - * Example: name of file with 2 contracts is {@code foo.groovy}, it will be - * converted by the implementation to {@code foo.json}. The recursive file - * converter will create two files {@code 0_foo.json} and {@code 1_foo.json} - */ - String generateOutputFileNameForInput(String inputFileName) -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.java new file mode 100644 index 0000000000..9cb601f1e8 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2019 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.cloud.contract.verifier.converter; + +import java.util.Map; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.verifier.file.ContractMetadata; + +/** + * Converts contracts into their stub representation. + * + * @since 1.1.0 + */ +public interface StubGenerator { + + /** + * @param fileName - file name + * @return {@code true} if the converter can handle the file to convert it into a + * stub. + */ + default boolean canHandleFileName(String fileName) { + return fileName.endsWith(fileExtension()); + } + + /** + * @param rootName - root name of the contract + * @param content - metadata of the contract + * @return the collection of converted contracts into stubs. One contract can result + * in multiple stubs. + */ + Map convertContents(String rootName, ContractMetadata content); + + /** + * @param inputFileName - name of the input file + * @return the name of the converted stub file. If you have multiple contracts in a + * single file then a prefix will be added to the generated file. If you provide the + * {@link Contract#name} field then that field will override the generated file name. + * + * Example: name of file with 2 contracts is {@code foo.groovy}, it will be converted + * by the implementation to {@code foo.json}. The recursive file converter will create + * two files {@code 0_foo.json} and {@code 1_foo.json} + */ + String generateOutputFileNameForInput(String inputFileName); + + /** + * Describes the file extension that this stub generator can handle. + * @return string describing the file extension + */ + default String fileExtension() { + return ".json"; + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy index cdfc9eb186..85fc32e44e 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.groovy @@ -28,11 +28,6 @@ import org.springframework.cloud.contract.verifier.converter.StubGenerator @CompileStatic abstract class DslToWireMockConverter implements StubGenerator { - @Override - boolean canHandleFileName(String fileName) { - return fileName.endsWith('.groovy') - } - @Override String generateOutputFileNameForInput(String inputFileName) { return inputFileName.replaceAll(extension(inputFileName), 'json') diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java index 7469d9f88a..1bef296575 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java @@ -38,10 +38,10 @@ import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConve import org.springframework.cloud.contract.verifier.converter.ToYamlConverter; /** - * Convert Spring Cloud Contract Verifier contracts into WireMock stubs mappings. + * Convert Spring Cloud Contract Verifier contracts into stubs mappings. *

* This goal allows you to generate `stubs-jar` or execute `spring-cloud-contract:run` - * with generated WireMock mappings. + * with generated mappings. * * @author Mariusz Smykula */ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java index 60f76fa4e4..f1cdf47823 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java @@ -47,9 +47,6 @@ import org.jetbrains.annotations.NotNull; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; -import org.springframework.cloud.contract.verifier.converter.StubGenerator; -import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider; -import org.springframework.cloud.contract.verifier.file.ContractMetadata; import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; @@ -216,32 +213,16 @@ class PactStubDownloader implements StubDownloader { log.debug("Converted pact file [" + file + "] to [" + contracts.size() + "] contracts"); } - StubGeneratorProvider provider = new StubGeneratorProvider(); - Collection stubGenerators = provider - .converterForName(ARTIFICIAL_NAME_ENDING_WITH_GROOVY); - if (log.isDebugEnabled()) { - log.debug("Found following matching stub generators " + stubGenerators); - } - for (StubGenerator stubGenerator : stubGenerators) { - Map map = stubGenerator.convertContents(file.getName(), - new ContractMetadata(file.toPath(), false, contracts.size(), null, - contracts)); - for (Map.Entry entry : map.entrySet()) { - String value = entry.getValue(); - File mapping = new File(mappingsFolder, - StringUtils.stripFilenameExtension(file.getName()) + "_" - + Math.abs(entry.getKey().hashCode()) + ".json"); - storeFile(mapping.toPath(), value.getBytes()); - } - } + MappingGenerator.toMappings(file, contracts, mappingsFolder); } - private void storeFile(Path path, byte[] contents) { + private Path storeFile(Path path, byte[] contents) { try { - Files.write(path, contents); + Path storedPath = Files.write(path, contents); if (log.isDebugEnabled()) { log.debug("Stored file [" + path.toString() + "]"); } + return storedPath; } catch (IOException e) { throw new IllegalStateException(e);