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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user