Fixed caching of downloaded contracts in Gradle

without this change when using a daemon, for snapshot versions of downloaded contracts allways the same folder is returned
with this change we cache only non snapshot artifacts; also there's a switch you can set to disable caching at all

fixes #343
This commit is contained in:
Marcin Grzejszczak
2017-07-03 12:52:48 +02:00
parent b54c36ce8b
commit 8c96a21abf
8 changed files with 194 additions and 14 deletions

View File

@@ -124,6 +124,7 @@ contracts {
} }
contractsPath = '' contractsPath = ''
contractsWorkOffline = false contractsWorkOffline = false
cacheDownloadedContracts = true
} }
tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateClientStubs') { 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 - **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. - **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 - **contractsWorkOffline** - if the dependencies should be downloaded or local Maven only should be reused
- **cacheDownloadedContracts** - if you want to reuse download JARs that contain contract definitions.
For complete information take a look at https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract-maven-plugin/plugin-info.html[Plugin Documentation] 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 ====== Single base class for all tests

View File

@@ -86,6 +86,10 @@ public class StubConfiguration {
return StringUtils.hasText(this.groupId) && StringUtils.hasText(this.artifactId); 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() { public String toColonSeparatedDependencyNotation() {
if (!isDefined()) { if (!isDefined()) {
return ""; return "";
@@ -102,6 +106,13 @@ public class StubConfiguration {
return StringUtils.hasText(value) ? value : ""; 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) { public boolean groupIdAndArtifactMatches(String ivyNotationAsString) {
String[] parts = ivyNotationFrom(ivyNotationAsString); String[] parts = ivyNotationFrom(ivyNotationAsString);
String groupId = parts[0]; String groupId = parts[0];
@@ -112,6 +123,14 @@ public class StubConfiguration {
return this.groupId.equals(groupId) && this.artifactId.equals(artifactId); 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() { public String getGroupId() {
return this.groupId; return this.groupId;
} }

View File

@@ -1,18 +1,20 @@
package org.springframework.cloud.contract.stubrunner package org.springframework.cloud.contract.stubrunner
import io.specto.hoverfly.junit.HoverflyRule import io.specto.hoverfly.junit.HoverflyRule
import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.RepositorySystemSession
import org.junit.Rule import org.junit.Rule
import org.springframework.util.ResourceUtils import org.springframework.util.ResourceUtils
import spock.lang.IgnoreIf
import spock.lang.Specification import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties import spock.util.environment.RestoreSystemProperties
class AetherStubDownloaderSpec extends Specification { class AetherStubDownloaderSpec extends Specification {
@Rule @Rule
HoverflyRule hoverflyRule = HoverflyRule.buildFromClassPathResource("simulation.json").build() HoverflyRule hoverflyRule = HoverflyRule.buildFromClassPathResource("simulation.json").build()
// 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'() { def 'Should be able to download from a repository using username and password authentication'() {
given: given:
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder() StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()

View File

@@ -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
}
} }

View File

@@ -141,6 +141,12 @@ class ContractVerifierExtension {
*/ */
boolean excludeBuildFolders 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) { void contractDependency(@DelegatesTo(Dependency) Closure closure) {
closure.delegate = contractDependency closure.delegate = contractDependency
closure.call() closure.call()

View File

@@ -1,5 +1,6 @@
package org.springframework.cloud.contract.verifier.plugin package org.springframework.cloud.contract.verifier.plugin
import groovy.transform.CompileStatic
import groovy.transform.PackageScope import groovy.transform.PackageScope
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
@@ -7,6 +8,7 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig
* @author Marcin Grzejszczak * @author Marcin Grzejszczak
*/ */
@PackageScope @PackageScope
@CompileStatic
class ExtensionToProperties { class ExtensionToProperties {
protected static ContractVerifierConfigProperties fromExtension(ContractVerifierExtension extension) { protected static ContractVerifierConfigProperties fromExtension(ContractVerifierExtension extension) {

View File

@@ -1,5 +1,6 @@
package org.springframework.cloud.contract.verifier.plugin package org.springframework.cloud.contract.verifier.plugin
import groovy.transform.CompileStatic
import groovy.transform.PackageScope import groovy.transform.PackageScope
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.logging.Logger import org.gradle.api.logging.Logger
@@ -15,13 +16,14 @@ import java.util.concurrent.ConcurrentHashMap
* @author Marcin Grzejszczak * @author Marcin Grzejszczak
*/ */
@PackageScope @PackageScope
@CompileStatic
class GradleContractsDownloader { class GradleContractsDownloader {
private static final String LATEST_VERSION = '+' private static final String LATEST_VERSION = '+'
private final Project project private final Project project
private final Logger log private final Logger log
private static final Map<StubConfiguration, File> downloadedContract = new ConcurrentHashMap<>() protected static final Map<StubConfiguration, File> downloadedContract = new ConcurrentHashMap<>()
GradleContractsDownloader(Project project, Logger log) { GradleContractsDownloader(Project project, Logger log) {
this.project = project this.project = project
@@ -31,24 +33,29 @@ class GradleContractsDownloader {
File downloadAndUnpackContractsIfRequired(ContractVerifierExtension extension, File downloadAndUnpackContractsIfRequired(ContractVerifierExtension extension,
ContractVerifierConfigProperties config) { ContractVerifierConfigProperties config) {
File defaultContractsDir = extension.contractsDslDir 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 // download contracts, unzip them and pass as output directory
if (shouldDownloadContracts(extension)) { if (shouldDownloadContracts(extension)) {
this.log.info("For project [${this.project.name}] Download dependency is provided - will download contract jars") this.log.info("For project [${this.project.name}] Download dependency is provided - will download contract jars")
this.log.info("Contract dependency [{}]", extension.contractDependency) this.log.info("Contract dependency [{}]", extension.contractDependency)
StubConfiguration configuration = stubConfiguration(extension.contractDependency) StubConfiguration configuration = stubConfiguration(extension.contractDependency)
this.log.info("Got the following contract dependency to download [{}]", configuration) this.log.info("Got the following contract dependency to download [{}]", configuration)
File cachedFolder = downloadedContract.get(configuration) this.log.info("The contract dependency is a changing one [{}] and cache download switch is set to [{}]",
if (cachedFolder) { configuration.isVersionChanging(), extension.cacheDownloadedContracts)
this.log.info("For project [${this.project.name}] Returning the cached location of the contracts") if (!configuration.isVersionChanging() && extension.cacheDownloadedContracts) {
contractDownloader(extension, configuration).updatePropertiesWithInclusion(cachedFolder, config) this.log.info("Resolved a non changing version - will try to return the folder from a cache")
return cachedFolder 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) File downloadedContracts = contractDownloader(extension, configuration).unpackedDownloadedContracts(config)
downloadedContract.put(configuration, downloadedContracts) downloadedContract.put(configuration, downloadedContracts)
return 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 return defaultContractsDir
} }
@@ -58,12 +65,12 @@ class GradleContractsDownloader {
StringUtils.hasText(extension.contractDependency.stringNotation)) 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, return new ContractDownloader(stubDownloader(extension), configuration,
extension.contractsPath, this.project.group as String, this.project.name) extension.contractsPath, this.project.group as String, this.project.name)
} }
private AetherStubDownloader stubDownloader(ContractVerifierExtension extension) { protected AetherStubDownloader stubDownloader(ContractVerifierExtension extension) {
return new AetherStubDownloader( return new AetherStubDownloader(
new StubRunnerOptionsBuilder() new StubRunnerOptionsBuilder()
.withStubRepositoryRoot(extension.contractsRepositoryUrl) .withStubRepositoryRoot(extension.contractsRepositoryUrl)

View File

@@ -1,6 +1,11 @@
package org.springframework.cloud.contract.verifier.plugin 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.stubrunner.StubConfiguration
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import spock.lang.Specification import spock.lang.Specification
/** /**
@@ -8,6 +13,9 @@ import spock.lang.Specification
*/ */
class GradleContractsDownloaderSpec extends Specification { class GradleContractsDownloaderSpec extends Specification {
Project project = Stub(Project)
Logger logger = Stub(Logger)
def "should parse dependency via string notation"() { def "should parse dependency via string notation"() {
given: given:
String stringNotation = "com.example:foo:1.0.0:stubs" String stringNotation = "com.example:foo:1.0.0:stubs"
@@ -37,6 +45,7 @@ class GradleContractsDownloaderSpec extends Specification {
stubConfig.version == "1.0.0" stubConfig.version == "1.0.0"
stubConfig.classifier == "stubs" stubConfig.classifier == "stubs"
} }
def "should parse dependency via string notation with methods"() { def "should parse dependency via string notation with methods"() {
given: given:
String stringNotation = "com.example:foo:1.0.0:stubs" String stringNotation = "com.example:foo:1.0.0:stubs"
@@ -66,4 +75,122 @@ class GradleContractsDownloaderSpec extends Specification {
stubConfig.version == "1.0.0" stubConfig.version == "1.0.0"
stubConfig.classifier == "stubs" 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")
}
} }