From 2be3d0ab8684ef55e36a0ed4bb5d9436290539be Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 6 Aug 2019 15:39:32 +0200 Subject: [PATCH] Added the option "inProgress" to the contract without this change we can't have a contract that will generate a stub but not a test. Of course that's for a reason since it's overriding the very essence of contract test. with this change we assume that the users know what they're doing and that they will use this flag rarerly. Cause it means that you can have a false positive. related to gh-881 --- .../com/example/loan/GoodbyeWorldTests.java | 2 +- .../cloud/contract/spec/Contract.java | 21 ++++++++ .../stubrunner/FileStubDownloader.java | 36 +++++++++++-- .../ResourceResolvingStubDownloader.java | 54 ++++++++++--------- .../stubrunner/TemporaryFileStorage.java | 21 +++++--- .../stubrunner/StubsStubDownloaderTests.java | 6 ++- .../contract/verifier/TestGenerator.groovy | 4 +- .../verifier/converter/ContractsToYaml.groovy | 1 + .../verifier/converter/YamlContract.java | 2 + .../verifier/converter/YamlToContracts.groovy | 3 ++ .../verifier/file/ContractMetadata.groovy | 4 ++ .../YamlContractConverterSpec.groovy | 4 ++ 12 files changed, 117 insertions(+), 41 deletions(-) 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 index 651f5e0e70..93de688655 100644 --- 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 @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.NONE) @AutoConfigureStubRunner(ids = { "com.example:http-server-dsl"}, - repositoryRoot = "stubs://classpath://contractsAtRuntime/", + repositoryRoot = "stubs://classpath:contractsAtRuntime/", stubsMode = StubRunnerProperties.StubsMode.LOCAL, generateStubs = true) public class GoodbyeWorldTests { diff --git a/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java b/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java index 8f7abe57d1..f8ee81b65d 100644 --- a/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java +++ b/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java @@ -89,6 +89,12 @@ public class Contract { */ private boolean ignored; + /** + * Whether the contract is in progress. It's not ignored, but the feature is not yet + * finished. Used together with the {@code generateStubs} option. + */ + private boolean inProgress; + public Contract() { } @@ -259,6 +265,17 @@ public class Contract { this.ignored = true; } + /** + * Whether the contract is in progress or not. + */ + public void inProgress() { + this.inProgress = true; + } + + public boolean isInProgress() { + return this.inProgress; + } + public Integer getPriority() { return priority; } @@ -335,6 +352,10 @@ public class Contract { this.ignored = ignored; } + public void setInProgress(boolean inProgress) { + this.inProgress = inProgress; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/FileStubDownloader.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/FileStubDownloader.java index 0b6174ebae..ec99e6b82a 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/FileStubDownloader.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/FileStubDownloader.java @@ -20,7 +20,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.util.AbstractMap; +import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -134,14 +134,42 @@ class StubsStubDownloader implements StubDownloader { boolean shouldFindProducer = shouldFindProducer(); if (!shouldFindProducer) { String schemeSpecific = schemeSpecificPart(); - log.info("Stubs are present under [" + schemeSpecific + "]"); - return new AbstractMap.SimpleEntry<>(stubConfiguration, - new File(schemeSpecific)); + log.info("Stubs are present under [" + schemeSpecific + + "]. Will copy them to a temporary directory."); + return new ResourceResolvingStubDownloader(stubRunnerOptions, + this::repoRootForSchemeSpecificPart, this::anyPattern) + .downloadAndUnpackStubJar(stubConfiguration); } return new ResourceResolvingStubDownloader(stubRunnerOptions, this::repoRoot, this::gavPattern).downloadAndUnpackStubJar(stubConfiguration); } + private RepoRoots repoRootForSchemeSpecificPart(StubRunnerOptions stubRunnerOptions, + StubConfiguration configuration) { + String specificPart = schemeSpecificPart(); + specificPart = specificPart.endsWith("/") ? specificPart : (specificPart + "/"); + specificPart = specificPart + "**/*.*"; + return new RepoRoots(Collections.singleton(new RepoRoot(specificPart))); + } + + private Pattern anyPattern(StubConfiguration config) { + return Pattern.compile(resolvePath() + "(.*)"); + } + + private String resolvePath() { + String schemeSpecificPart = schemeSpecificPart(); + Resource resource = ResourceResolver.resource(schemeSpecificPart); + if (resource != null) { + try { + return Paths.get(resource.getURI()).toString(); + } + catch (IOException ex) { + return schemeSpecificPart; + } + } + return schemeSpecificPart; + } + // for group id a.b.c and artifact id d // a.b.c/d // a/b/c/d diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ResourceResolvingStubDownloader.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ResourceResolvingStubDownloader.java index f14759e184..392ca3f67c 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ResourceResolvingStubDownloader.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ResourceResolvingStubDownloader.java @@ -40,17 +40,12 @@ import shaded.com.google.common.base.Function; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.util.StringUtils; class ResourceResolvingStubDownloader implements StubDownloader { private static final Log log = LogFactory .getLog(ResourceResolvingStubDownloader.class); - private static final int TEMP_DIR_ATTEMPTS = 10000; - - private static final String LATEST_VERSION = "+"; - private final StubRunnerOptions stubRunnerOptions; private final BiFunction repoRootFunction; @@ -71,6 +66,7 @@ class ResourceResolvingStubDownloader implements StubDownloader { @Override public Map.Entry downloadAndUnpackStubJar( StubConfiguration config) { + registerShutdownHook(); List repoRoots = repoRootFunction.apply(stubRunnerOptions, config); List paths = toPaths(repoRoots); List resources = resolveResources(paths); @@ -81,7 +77,7 @@ class ResourceResolvingStubDownloader implements StubDownloader { throw new IllegalStateException("No stubs were found on classpath for [" + config.getGroupId() + ":" + config.getArtifactId() + "]"); } - final File tmp = createTempDir(); + final File tmp = TemporaryFileStorage.createTempDir("classpath-stubs"); if (stubRunnerOptions.isDeleteStubsAfterTest()) { tmp.deleteOnExit(); } @@ -93,7 +89,7 @@ class ResourceResolvingStubDownloader implements StubDownloader { if (log.isDebugEnabled()) { log.debug("Relative path for resource is [" + relativePath + "]"); } - if (StringUtils.isEmpty(relativePath)) { + if (relativePath == null) { log.warn("Unable to match the URI [" + resource.getURI() + "]"); continue; } @@ -117,6 +113,11 @@ class ResourceResolvingStubDownloader implements StubDownloader { tmp); } + private void registerShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> TemporaryFileStorage + .cleanup(stubRunnerOptions.isDeleteStubsAfterTest()))); + } + private void copyTheFoundFiles(File tmp, Resource resource, String relativePath) throws IOException { // the relative path is OS agnostic and contains / only @@ -133,6 +134,7 @@ class ResourceResolvingStubDownloader implements StubDownloader { if (!newFile.exists() && !isDirectory(resource)) { try (InputStream stream = resource.getInputStream()) { Files.copy(stream, newFile.toPath()); + TemporaryFileStorage.add(newFile); } } if (log.isDebugEnabled()) { @@ -157,14 +159,29 @@ class ResourceResolvingStubDownloader implements StubDownloader { String relativePathPicker(Resource resource, Pattern groupAndArtifactPattern) throws IOException { - String uri = resource.getURI().toString(); - Matcher groupAndArtifactMatcher = groupAndArtifactPattern.matcher(uri); - if (groupAndArtifactMatcher.matches()) { + Matcher groupAndArtifactMatcher = matcher(resource, groupAndArtifactPattern); + if (groupAndArtifactMatcher.matches() + && groupAndArtifactMatcher.groupCount() > 2) { MatchResult groupAndArtifactResult = groupAndArtifactMatcher.toMatchResult(); return groupAndArtifactResult.group(2) + groupAndArtifactResult.group(3); } + else if (groupAndArtifactMatcher.matches()) { + return groupAndArtifactMatcher.group(1); + } else { - return ""; + return null; + } + } + + private Matcher matcher(Resource resource, Pattern groupAndArtifactPattern) + throws IOException { + try { + String path = resource.getURI().getPath(); + return groupAndArtifactPattern.matcher(path); + } + catch (Exception ex) { + String path = resource.getURI().toString(); + return groupAndArtifactPattern.matcher(path); } } @@ -192,21 +209,6 @@ class ResourceResolvingStubDownloader implements StubDownloader { return resources; } - // Taken from Guava - private File createTempDir() { - File baseDir = new File(System.getProperty("java.io.tmpdir")); - String baseName = System.currentTimeMillis() + "-"; - for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { - File tempDir = new File(baseDir, baseName + counter); - if (tempDir.mkdir()) { - return tempDir; - } - } - throw new IllegalStateException("Failed to create directory within " - + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName - + (TEMP_DIR_ATTEMPTS - 1) + ")"); - } - } class RepoRoot { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/TemporaryFileStorage.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/TemporaryFileStorage.java index cbfe01f534..d998e77aa3 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/TemporaryFileStorage.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/TemporaryFileStorage.java @@ -29,8 +29,6 @@ import java.util.concurrent.LinkedBlockingQueue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import static java.nio.file.Files.createTempDirectory; - /** * Stores all generated temporary folders with stubs. * @@ -41,6 +39,8 @@ final class TemporaryFileStorage { private static final Log log = LogFactory.getLog(TemporaryFileStorage.class); + private static final int TEMP_DIR_ATTEMPTS = 10000; + /** * There are problems with removal of stubs unpacked to a temporary folder. That's why * we're creating a bounded in-memory storage of unpacked files and later we register @@ -104,14 +104,19 @@ final class TemporaryFileStorage { } } + // taken from Guava static File createTempDir(String tempDirPrefix) { - try { - return createTempDirectory(tempDirPrefix).toFile(); - } - catch (IOException e) { - throw new IllegalStateException( - "Cannot create tmp dir with prefix: [" + tempDirPrefix + "]", e); + File baseDir = new File(System.getProperty("java.io.tmpdir")); + String baseName = tempDirPrefix + "-" + System.currentTimeMillis() + "-"; + for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { + File tempDir = new File(baseDir, baseName + counter); + if (tempDir.mkdir()) { + return tempDir; + } } + throw new IllegalStateException("Failed to create directory within " + + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName + + (TEMP_DIR_ATTEMPTS - 1) + ")"); } } diff --git a/spring-cloud-contract-stub-runner/src/test/java/org/springframework/cloud/contract/stubrunner/StubsStubDownloaderTests.java b/spring-cloud-contract-stub-runner/src/test/java/org/springframework/cloud/contract/stubrunner/StubsStubDownloaderTests.java index 00cda7106a..f2dc08151f 100644 --- a/spring-cloud-contract-stub-runner/src/test/java/org/springframework/cloud/contract/stubrunner/StubsStubDownloaderTests.java +++ b/spring-cloud-contract-stub-runner/src/test/java/org/springframework/cloud/contract/stubrunner/StubsStubDownloaderTests.java @@ -39,7 +39,11 @@ public class StubsStubDownloaderTests { .downloadAndUnpackStubJar(new StubConfiguration("lv.spring.cloud:bye")); BDDAssertions.then(entry).isNotNull(); - BDDAssertions.then(entry.getValue().getPath()).endsWith("repository/mappings"); + BDDAssertions.then(entry.getValue()).exists(); + BDDAssertions.then(new File(entry.getValue(), "pl/spring/cloud/bye/pl_bye.json")) + .exists(); + BDDAssertions.then(new File(entry.getValue(), "lv/spring/cloud/bye/lv_bye.json")) + .exists(); } @Test diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy index 2c5ba80eea..47fa5e6cba 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy @@ -113,7 +113,9 @@ class TestGenerator { void generateTestClasses(final String basePackageName) { ListMultimap contracts = contractFileScanner. findContracts() - contracts.asMap().entrySet().each { + contracts.asMap().entrySet() + .findAll { Map.Entry> entry -> !entry.value.any { it.anyInProgress() }} + .each { Map.Entry> entry -> processIncludedDirectory( relativizeContractPath(entry), (Collection) entry. diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy index 029261164f..9b4caa540f 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy @@ -58,6 +58,7 @@ class ContractsToYaml { } yamlContract.name = contract.name yamlContract.ignored = contract.ignored + yamlContract.inProgress = contract.inProgress yamlContract.description = contract.description yamlContract.label = contract.label request(contract, yamlContract) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java index 3bb2b75686..6e2f3f22b7 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java @@ -52,6 +52,8 @@ public class YamlContract { public boolean ignored; + public boolean inProgress; + public static class Request { public String method; diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy index 5ac695948d..81a20ae0e9 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy @@ -98,6 +98,9 @@ class YamlToContracts { if (yamlContract.ignored) { ignored() } + if (yamlContract.inProgress) { + inProgress() + } if (yamlContract.request?.method) { request { method(yamlContract.request?.method) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy index 7315eae8db..25239502f4 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractMetadata.groovy @@ -86,6 +86,10 @@ class ContractMetadata { return this.convertedContractWithMetadata .find { it.contract == contract } } + + boolean anyInProgress() { + return this.convertedContract.any { it.inProgress } + } } @CompileStatic diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index 181a8041d4..d948b4ab54 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -716,6 +716,7 @@ label: null name: "post1" priority: null ignored: false +inProgress: false ''' String expectedYaml2 = '''\ --- @@ -757,6 +758,7 @@ label: null name: "post2" priority: null ignored: false +inProgress: false ''' when: Map strings = converter.store([ @@ -985,6 +987,7 @@ ignored: false name("fooo") label("card_rejected") ignored() + inProgress() input { messageFrom("input") messageBody([ @@ -1093,6 +1096,7 @@ ignored: false YamlContract yamlContract = yamlContracts.first() yamlContract.name == "fooo" yamlContract.ignored == true + yamlContract.inProgress == true yamlContract.label == "card_rejected" yamlContract.input.messageFrom == "input" yamlContract.input.messageBody == [