diff --git a/docs/src/main/asciidoc/verifier/rest.adoc b/docs/src/main/asciidoc/verifier/rest.adoc index a009dca313..a4c2644f57 100644 --- a/docs/src/main/asciidoc/verifier/rest.adoc +++ b/docs/src/main/asciidoc/verifier/rest.adoc @@ -124,6 +124,7 @@ contracts { } contractsPath = '' contractsWorkOffline = false + cacheDownloadedContracts = true } tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateClientStubs') { @@ -443,8 +444,9 @@ If you want to download your contract definitions from a Maven repository you ca - **contractDependency** - the contract dependency that contains all the packaged contracts - **contractsPath** - path to concrete contracts in the JAR with packaged contracts. Defaults to `groupid/artifactid` where `gropuid` is slash separated. - **contractsWorkOffline** - if the dependencies should be downloaded or local Maven only should be reused - -For complete information take a look at https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract-maven-plugin/plugin-info.html[Plugin Documentation] + - **cacheDownloadedContracts** - if you want to reuse download JARs that contain contract definitions. + We cache only non-snapshot, explicitly provided versions (e.g. `+` or `1.0.0.BUILD-SNAPSHOT` won't get cached). + By default this feature is turned on. ====== Single base class for all tests diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubConfiguration.java index c8f2e2ad4f..d2aad87027 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubConfiguration.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubConfiguration.java @@ -86,6 +86,10 @@ public class StubConfiguration { return StringUtils.hasText(this.groupId) && StringUtils.hasText(this.artifactId); } + /** + * Returns a colon separated representation of the stub configuration + * (e.g. groupid:artifactid:version:classifier) + */ public String toColonSeparatedDependencyNotation() { if (!isDefined()) { return ""; @@ -102,6 +106,13 @@ public class StubConfiguration { return StringUtils.hasText(value) ? value : ""; } + /** + * Checks if ivy notation matches group and artifact ids + * + * @param ivyNotationAsString - e.g. group:artifact:version:classifier + * @return {@code true} if artifact id matches and there's no group id. Or if + * both group id and artifact id are present and matching + */ public boolean groupIdAndArtifactMatches(String ivyNotationAsString) { String[] parts = ivyNotationFrom(ivyNotationAsString); String groupId = parts[0]; @@ -112,6 +123,14 @@ public class StubConfiguration { return this.groupId.equals(groupId) && this.artifactId.equals(artifactId); } + /** + * Returns {@code true} for a snapshot or a LATEST (+) version + */ + public boolean isVersionChanging() { + return DEFAULT_VERSION.equals(this.version) || + this.version.toLowerCase().contains("snapshot"); + } + public String getGroupId() { return this.groupId; } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloaderSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloaderSpec.groovy index ed0a25795e..1abe22ee63 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloaderSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/AetherStubDownloaderSpec.groovy @@ -1,18 +1,20 @@ package org.springframework.cloud.contract.stubrunner import io.specto.hoverfly.junit.HoverflyRule -import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RepositorySystemSession import org.junit.Rule import org.springframework.util.ResourceUtils +import spock.lang.IgnoreIf import spock.lang.Specification import spock.util.environment.RestoreSystemProperties - class AetherStubDownloaderSpec extends Specification { @Rule HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode("simulation.json") + // CI tools sometimes can't reach the `test.jfrog.io` address + @IgnoreIf({ Boolean.valueOf(env['CI']) }) def 'Should be able to download from a repository using username and password authentication'() { given: StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder() diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy index 1c7602f57e..76678c7d4a 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubConfigurationSpec.groovy @@ -45,4 +45,19 @@ class StubConfigurationSpec extends Specification { } + @Unroll + def 'should resolve [#ivy] as a changing version [#result]'() { + given: + StubConfiguration stubConfiguration = new StubConfiguration(ivy) + expect: + result == stubConfiguration.isVersionChanging() + where: + ivy || result + 'group:artifact:1.0.0.RELEASE:classifier' || false + 'group:artifact:1.0.0.BUILD-SNAPSHOT:' || true + 'group:artifact:1.0.0.SNAPSHOT' || true + 'group:artifact:+:' || true + + } + } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy index 94ab514434..6dff15a8ff 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy @@ -141,6 +141,12 @@ class ContractVerifierExtension { */ boolean excludeBuildFolders + /** + * If set to true then will cache the folder where non snapshot contract artifacts + * got downloaded. + */ + boolean cacheDownloadedContracts = true + void contractDependency(@DelegatesTo(Dependency) Closure closure) { closure.delegate = contractDependency closure.call() diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy index b9be34cb9a..efdb8c2ad2 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy @@ -1,5 +1,6 @@ package org.springframework.cloud.contract.verifier.plugin +import groovy.transform.CompileStatic import groovy.transform.PackageScope import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties @@ -7,6 +8,7 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig * @author Marcin Grzejszczak */ @PackageScope +@CompileStatic class ExtensionToProperties { protected static ContractVerifierConfigProperties fromExtension(ContractVerifierExtension extension) { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloader.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloader.groovy index ad95cd6715..309a78acc7 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloader.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloader.groovy @@ -1,11 +1,14 @@ package org.springframework.cloud.contract.verifier.plugin +import groovy.transform.CompileStatic import groovy.transform.PackageScope import org.gradle.api.Project import org.gradle.api.logging.Logger import org.springframework.cloud.contract.stubrunner.AetherStubDownloader import org.springframework.cloud.contract.stubrunner.ContractDownloader import org.springframework.cloud.contract.stubrunner.StubConfiguration +import org.springframework.cloud.contract.stubrunner.StubDownloader +import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilderProvider import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.util.StringUtils @@ -15,13 +18,14 @@ import java.util.concurrent.ConcurrentHashMap * @author Marcin Grzejszczak */ @PackageScope +@CompileStatic class GradleContractsDownloader { private static final String LATEST_VERSION = '+' private final Project project private final Logger log - private static final Map downloadedContract = new ConcurrentHashMap<>() + protected static final Map downloadedContract = new ConcurrentHashMap<>() GradleContractsDownloader(Project project, Logger log) { this.project = project @@ -31,24 +35,29 @@ class GradleContractsDownloader { File downloadAndUnpackContractsIfRequired(ContractVerifierExtension extension, ContractVerifierConfigProperties config) { File defaultContractsDir = extension.contractsDslDir - this.log.info("Project has group id [${this.project.group}], artifact id [${this.project.name}]") + this.log.info("Project has group id [{}], artifact id [{}]", this.project.group, this.project.name) // download contracts, unzip them and pass as output directory if (shouldDownloadContracts(extension)) { this.log.info("For project [${this.project.name}] Download dependency is provided - will download contract jars") this.log.info("Contract dependency [{}]", extension.contractDependency) StubConfiguration configuration = stubConfiguration(extension.contractDependency) this.log.info("Got the following contract dependency to download [{}]", configuration) - File cachedFolder = downloadedContract.get(configuration) - if (cachedFolder) { - this.log.info("For project [${this.project.name}] Returning the cached location of the contracts") - contractDownloader(extension, configuration).updatePropertiesWithInclusion(cachedFolder, config) - return cachedFolder + this.log.info("The contract dependency is a changing one [{}] and cache download switch is set to [{}]", + configuration.isVersionChanging(), extension.cacheDownloadedContracts) + if (!configuration.isVersionChanging() && extension.cacheDownloadedContracts) { + this.log.info("Resolved a non changing version - will try to return the folder from a cache") + File cachedFolder = downloadedContract.get(configuration) + if (cachedFolder) { + this.log.info("For project [{}] returning the cached location of the contracts", this.project.name) + contractDownloader(extension, configuration).updatePropertiesWithInclusion(cachedFolder, config) + return cachedFolder + } } File downloadedContracts = contractDownloader(extension, configuration).unpackedDownloadedContracts(config) downloadedContract.put(configuration, downloadedContracts) return downloadedContracts } - this.log.info("For project [${this.project.name}] will use contracts provided in the folder [" + defaultContractsDir + "]") + this.log.info("For project [{}] will use contracts provided in the folder [{}]", this.project.name, defaultContractsDir) return defaultContractsDir } @@ -58,13 +67,14 @@ class GradleContractsDownloader { StringUtils.hasText(extension.contractDependency.stringNotation)) } - private ContractDownloader contractDownloader(ContractVerifierExtension extension, StubConfiguration configuration) { + protected ContractDownloader contractDownloader(ContractVerifierExtension extension, StubConfiguration configuration) { return new ContractDownloader(stubDownloader(extension), configuration, extension.contractsPath, this.project.group as String, this.project.name) } - private AetherStubDownloader stubDownloader(ContractVerifierExtension extension) { - return new AetherStubDownloader( + protected StubDownloader stubDownloader(ContractVerifierExtension extension) { + StubDownloaderBuilderProvider provider = new StubDownloaderBuilderProvider() + return provider.getOrDefaultDownloader( new StubRunnerOptionsBuilder() .withStubRepositoryRoot(extension.contractsRepositoryUrl) .withWorkOffline(extension.contractsWorkOffline) diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloaderSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloaderSpec.groovy index 00c66585e0..0e32053dc7 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloaderSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/GradleContractsDownloaderSpec.groovy @@ -1,6 +1,11 @@ package org.springframework.cloud.contract.verifier.plugin +import org.gradle.api.Project +import org.gradle.api.logging.Logger +import org.springframework.cloud.contract.stubrunner.AetherStubDownloader +import org.springframework.cloud.contract.stubrunner.ContractDownloader import org.springframework.cloud.contract.stubrunner.StubConfiguration +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import spock.lang.Specification /** @@ -8,6 +13,9 @@ import spock.lang.Specification */ class GradleContractsDownloaderSpec extends Specification { + Project project = Stub(Project) + Logger logger = Stub(Logger) + def "should parse dependency via string notation"() { given: String stringNotation = "com.example:foo:1.0.0:stubs" @@ -37,6 +45,7 @@ class GradleContractsDownloaderSpec extends Specification { stubConfig.version == "1.0.0" stubConfig.classifier == "stubs" } + def "should parse dependency via string notation with methods"() { given: String stringNotation = "com.example:foo:1.0.0:stubs" @@ -66,4 +75,122 @@ class GradleContractsDownloaderSpec extends Specification { stubConfig.version == "1.0.0" stubConfig.classifier == "stubs" } + + def "should pick dependency from cache for a non snapshot contract dependency"() { + given: + ContractVerifierExtension ext = new ContractVerifierExtension() + ext.with { + contractDependency { + groupId("com.example") + artifactId("foo") + version("1.0.0") + classifier("stubs") + } + contractsRepositoryUrl = "foo" + } + and: + final AetherStubDownloader downloader = Mock(AetherStubDownloader) + final ContractDownloader contractDownloader = Mock(ContractDownloader) + and: + def gradleDownloader = stubbedContractDownloader(downloader, contractDownloader) + and: + StubConfiguration expectedStubConfig = new StubConfiguration("com.example:foo:1.0.0:stubs") + File expectedFileFromCache = new File("foo/bar") + GradleContractsDownloader.downloadedContract.put(expectedStubConfig, expectedFileFromCache) + when: + File file = gradleDownloader.downloadAndUnpackContractsIfRequired(ext, new ContractVerifierConfigProperties()) + then: + file == expectedFileFromCache + } + + def "should not pick dependency from cache for a non snapshot contract dependency with cache switch off"() { + given: + ContractVerifierExtension ext = new ContractVerifierExtension() + ext.with { + contractDependency { + groupId("com.example") + artifactId("foo") + version("1.0.0") + classifier("stubs") + } + contractsRepositoryUrl = "foo" + cacheDownloadedContracts = false + } + and: + final AetherStubDownloader downloader = Mock(AetherStubDownloader) + final ContractDownloader contractDownloader = Mock(ContractDownloader) + and: + def gradleDownloader = stubbedContractDownloader(downloader, contractDownloader) + and: + StubConfiguration expectedStubConfig = new StubConfiguration("com.example:foo:1.0.0:stubs") + File expectedFileFromCache = new File("foo/bar") + GradleContractsDownloader.downloadedContract.put(expectedStubConfig, expectedFileFromCache) + and: + File expectedFileNotFromCache = new File("foo/bar/baz") + contractDownloader.unpackedDownloadedContracts(_) >> expectedFileNotFromCache + when: + File file = gradleDownloader.downloadAndUnpackContractsIfRequired(ext, new ContractVerifierConfigProperties()) + then: + file == expectedFileNotFromCache + } + + def "should not pick dependency from cache for snapshot contract dependency"() { + given: + ContractVerifierExtension ext = new ContractVerifierExtension() + ext.with { + contractDependency { + groupId("com.example") + artifactId("foo") + version("1.0.0.BUILD-SNAPSHOT") + classifier("stubs") + } + contractsRepositoryUrl = "foo" + } + and: + final AetherStubDownloader downloader = Mock(AetherStubDownloader) + final ContractDownloader contractDownloader = Mock(ContractDownloader) + File expectedFileNotFromCache = new File("foo/bar/baz") + contractDownloader.unpackedDownloadedContracts(_) >> expectedFileNotFromCache + and: + def gradleDownloader = stubbedContractDownloader(downloader, contractDownloader) + when: + File file = gradleDownloader.downloadAndUnpackContractsIfRequired(ext, new ContractVerifierConfigProperties()) + then: + file == expectedFileNotFromCache + } + + private GradleContractsDownloader stubbedContractDownloader(downloader, contractDownloader) { + new GradleContractsDownloader(project, logger) { + @Override + protected AetherStubDownloader stubDownloader(ContractVerifierExtension extension) { + return downloader + } + + @Override + protected ContractDownloader contractDownloader(ContractVerifierExtension extension, StubConfiguration configuration) { + return contractDownloader + } + } + } + + def "should pick contract directory location from extension"() { + given: + ContractVerifierExtension ext = new ContractVerifierExtension() + ext.with { + contractsDslDir = new File("/foo/bar/baz") + } + and: + final AetherStubDownloader downloader = Mock(AetherStubDownloader) + final ContractDownloader contractDownloader = Mock(ContractDownloader) + and: + def gradleDownloader = stubbedContractDownloader(downloader, contractDownloader) + and: + StubConfiguration expectedStubConfig = new StubConfiguration("com.example:foo:1.0.0:stubs") + GradleContractsDownloader.downloadedContract.put(expectedStubConfig, new File("foo/bar")) + when: + File file = gradleDownloader.downloadAndUnpackContractsIfRequired(ext, new ContractVerifierConfigProperties()) + then: + file == new File("/foo/bar/baz") + } + }