org.gradle.api.CrossBuildScriptCachingIntegrationSpec.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-api Show documentation
Show all versions of gradle-api Show documentation
Gradle 6.9.1 API redistribution.
/*
* Copyright 2016 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
*
* http://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.gradle.api
import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.integtests.fixtures.BuildOperationTreeQueries
import org.gradle.integtests.fixtures.BuildOperationsFixture
import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache
import org.gradle.integtests.fixtures.ToBeFixedForIsolatedProjects
import org.gradle.integtests.fixtures.daemon.DaemonLogsAnalyzer
import org.gradle.integtests.fixtures.daemon.DaemonsFixture
import org.gradle.internal.scripts.CompileScriptBuildOperationType
import org.gradle.test.fixtures.ConcurrentTestUtil
import org.gradle.test.fixtures.file.TestFile
import org.gradle.test.fixtures.server.http.BlockingHttpServer
import org.gradle.util.GradleVersion
import org.junit.Rule
import spock.lang.Issue
import java.util.regex.Pattern
import static org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache.Skip.INVESTIGATE
class CrossBuildScriptCachingIntegrationSpec extends AbstractIntegrationSpec {
FileTreeBuilder root
File cachesDir
File scriptCachesDir
File remappedCachesDir
@Rule
BlockingHttpServer server = new BlockingHttpServer()
def buildOperations = new BuildOperationsFixture(executer, testDirectoryProvider)
private TestFile homeDirectory = testDirectoryProvider.getTestDirectory().file("user-home")
def setup() {
executer.requireOwnGradleUserHomeDir()
root = new FileTreeBuilder(testDirectory)
cachesDir = new File(homeDirectory, 'caches')
def versionCaches = new File(cachesDir, GradleVersion.current().version)
scriptCachesDir = new File(versionCaches, 'scripts')
remappedCachesDir = new File(versionCaches, 'scripts-remapped')
}
def "identical build files are compiled once"() {
given:
root {
core {
'core.gradle'(this.simpleBuild())
}
module1 {
'module1.gradle'(this.simpleBuild())
}
'settings.gradle'(this.settingsWithBuildScriptsUseProjectName('core', 'module1'))
}
when:
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 3
hasScript("settings", scripts)
hasScript(":core", scripts)
hasScript(":module1", scripts)
eachScriptIsUnique(scripts)
getCompileBuildFileOperationsCount() == 4 // classpath + body for settings and for the 2 identical scripts
}
@ToBeFixedForConfigurationCache(because = "test expect script evaluation")
def "identical build files are compiled once for distinct invocations"() {
given:
root {
core {
'core.gradle'(this.simpleBuild())
}
module1 {
'module1.gradle'(this.simpleBuild())
}
'settings.gradle'(this.settingsWithBuildScriptsUseProjectName('core', 'module1'))
}
when:
executer.withGradleUserHomeDir(homeDirectory)
executer.requireDaemon()
executer.requireIsolatedDaemons()
run 'help'
then:
getCompileBuildFileOperationsCount() == 4 // classpath + body for settings and for the 2 identical scripts
when:
def before = scriptDetails()
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 3
scriptsAreReused(before, scripts)
getCompileBuildFileOperationsCount() == 0
}
def "can have two build files with same contents and file name"() {
given:
root {
core {
'build.gradle'(this.simpleBuild())
}
module1 {
'build.gradle'(this.simpleBuild())
}
'settings.gradle'(this.settings('core', 'module1'))
}
when:
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 3
hasScript("settings", scripts)
hasScript(":core", scripts)
hasScript(":module1", scripts)
eachScriptIsUnique(scripts)
getCompileBuildFileOperationsCount() == 4 // classpath and body for settings and for the 2 identical scripts
}
def "can have two build files with different contents and same file name"() {
given:
root {
core {
'build.gradle'(this.simpleBuild())
}
module1 {
'build.gradle'(this.simpleBuild('// different contents'))
}
'settings.gradle'(this.settings('core', 'module1'))
}
when:
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 3
hasScript("settings", scripts)
hasScript(":core", scripts)
hasScript(":module1", scripts)
eachScriptIsUnique(scripts)
getCompileBuildFileOperationsCount() == 6 // classpath + body for settings and for each build.gradle file
}
def "reuses scripts when build file changes in a way that does not affect behaviour"() {
given:
root {
core {
'build.gradle'(this.simpleBuild())
}
module1 {
'build.gradle'(this.simpleBuild())
}
'settings.gradle'(this.settings('core', 'module1'))
}
when:
run 'help'
then:
getCompileBuildFileOperationsCount() == 4 // classpath + body for settings and for each build.gradle file
when:
def before = scriptDetails()
file('module1/build.gradle').text = simpleBuild('// different')
run 'help'
then:
def scripts = scriptDetails()
scriptsAreReused(before, scripts)
getCompileBuildFileOperationsCount() == 2 // classpath + body changed build.gradle file
}
def "remapping scripts doesn't mix up classes with same name"() {
given:
root {
'build.gradle'('''
task greet()
apply from: "gradle/one.gradle"
apply from: "gradle/two.gradle"
''')
gradle {
'one.gradle'('''
class Greeter { String toString() { 'Greetings from One!' } }
greet.doLast() { println new Greeter() }
''')
'two.gradle'('''
class Greeter { String toString() { 'Greetings from Two!' } }
greet.doLast() { println new Greeter() }
''')
}
}
when:
run 'greet'
then:
outputContains 'Greetings from One!'
outputContains 'Greetings from Two!'
}
@ToBeFixedForIsolatedProjects(because = "Investigate")
def "reports errors at the correct location when 2 scripts are identical"() {
given:
root {
module1 {
'module1.gradle'(this.taskThrowingError())
}
module2 {
'module2.gradle'(this.taskThrowingError())
}
'settings.gradle'(this.settingsWithBuildScriptsUseProjectName('module1', 'module2'))
}
when:
fails 'module1:someTask'
then:
def scripts = scriptDetails()
scripts.size() == 3
eachScriptIsUnique(scripts)
getCompileBuildFileOperationsCount() == 4 // classpath + body for settings and for the 2 identical scripts
and:
def module1File = file("module1/module1.gradle")
failure.assertHasFileName("Build file '$module1File'")
failure.assertHasLineNumber(5)
when:
fails 'module2:someTask'
then:
def module2File = file("module2/module2.gradle")
failure.assertHasFileName("Build file '$module2File'")
failure.assertHasLineNumber(5)
}
def "caches scripts applied from remote locations"() {
server.start()
given:
root {
'build.gradle'(this.applyFromRemote(server))
}
server.expect(server.get("shared.gradle").send("""
${instrument('"shared"')}
println 'Echo'
"""))
when:
run 'help'
then:
outputContains 'Echo'
and:
def scripts = scriptDetails()
scripts.size() == 2
hasScript(":", scripts)
hasScript("shared", scripts)
eachScriptIsUnique(scripts)
getCompileBuildFileOperationsCount() == 4 // classpath + body for each build script
}
def "caches scripts applied from remote locations when remote script changes"() {
server.start()
given:
root {
'build.gradle'(this.applyFromRemote(server))
}
server.expect(server.get("shared.gradle").send("""
${instrument('"shared"')}
println 'Echo 0'
"""))
when:
run 'help'
then:
outputContains 'Echo 0'
getCompileBuildFileOperationsCount() == 4 // classpath + body for build.gradle and shared build script
when:
def before = scriptDetails()
server.expect(server.head("shared.gradle"))
server.expect(server.get("shared.gradle").send("""
${instrument('"shared"')}
println 'Echo 1'
"""))
run 'help'
then:
outputContains 'Echo 1'
def scripts = scriptDetails()
scriptHasChanged("shared", before, scripts)
getCompileBuildFileOperationsCount() == 2 // classpath + body for shared build script
}
@Issue("GRADLE-2795")
def "can change script while build is running"() {
server.start()
given:
buildFile << simpleBuild("""
tasks.register("someLongRunningTask") {
doLast {
${server.callFromBuild("running")}
}
}
""")
def handle = server.expectAndBlock("running")
when:
def longRunning = executer.withTasks("someLongRunningTask").start()
handle.waitForAllPendingCalls()
then:
def before
ConcurrentTestUtil.poll {
// Output is delivered asynchronously, so wait until the details are available
before = scriptDetails(longRunning.standardOutput)
assert !before.isEmpty()
}
when:
buildFile << """
tasks.register("fastTask") {}
"""
// Don't write to the same file as the first process, as that blocks execution on Windows
def fastBuildOperations = new BuildOperationsFixture(executer, testDirectoryProvider, "operations-2")
def fast = executer.withTasks("fastTask").run()
assert longRunning.isRunning()
handle.releaseAll()
longRunning.waitForExit()
then:
def scripts = scriptDetails(fast.output)
scriptHasChanged(":", before, scripts)
getCompileBuildFileOperationsCount(fastBuildOperations) == 2 // classpath + body for fast task
getCompileBuildFileOperationsCount() == 2 // classpath + body for long running task
}
@ToBeFixedForConfigurationCache(because = "changing buildscript files dependency")
def "build script is recompiled when project's classpath changes"() {
createJarWithProperties("lib/foo.jar", [source: 1])
root {
'build.gradle'(simpleBuild('''
buildscript {
dependencies {
classpath files('lib/foo.jar')
}
}
'''))
}
when:
run 'help'
then:
getCompileBuildFileOperationsCount() == 2 // classpath + body for build.gradle
when:
def before = scriptDetails()
createJarWithProperties("lib/foo.jar", [target: 2])
run 'help'
then:
def scripts = scriptDetails()
scriptsAreReused(before, scripts) // The scripts end up with the same byte code and so the result is reused
getCompileBuildFileOperationsCount() == 1 // single classpath block
}
@ToBeFixedForConfigurationCache(because = "changing buildscript files dependency")
def "build script is recompiled when parent project's classpath changes"() {
createJarWithProperties("lib/foo.jar", [source: 1])
root {
'build.gradle'('''
buildscript {
dependencies {
classpath files('lib/foo.jar')
}
}
''')
module {
'module.gradle'(this.simpleBuild())
}
'settings.gradle'(this.settingsWithBuildScriptsUseProjectName('module'))
}
when:
run 'help'
then:
getCompileBuildFileOperationsCount() == 6 // classpath and body for settings and two build scripts
when:
def before = scriptDetails()
createJarWithProperties("lib/foo.jar", [target: 2])
run 'help'
then:
def scripts = scriptDetails()
scriptsAreReused(before, scripts) // scripts end up with the same byte code and so are reused
getCompileBuildFileOperationsCount() == 2 // body for two build scripts
}
def "init script is cached"() {
root {
'build.gradle'(this.simpleBuild())
gradle {
'init.gradle'("""
// init script
${this.instrument('"init"')}
""")
}
}
when:
executer.withArgument('-Igradle/init.gradle')
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 2
hasScript('init', scripts)
getCompileBuildFileOperationsCount() == 4 // classpath and body for build script and init script
}
def "same script can be applied from init script, settings script and build script"() {
root {
'common.gradle'("""
${this.instrument('"common"')}
""")
'init.gradle'('''
// init script
apply from: 'common.gradle'
''')
'settings.gradle'('''
// settings script
apply from: 'common.gradle'
''')
'build.gradle'('''
// build script
apply from: 'common.gradle'
''')
}
when:
executer.withArgument('-Iinit.gradle')
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 3 // same script applied 3 times
scripts.collect { it.className }.unique().size() == 1
scripts.collect { it.classpath }.unique().size() == 1
getCompileBuildFileOperationsCount() == 8 // classpath and body for each script
}
def "same script can be applied from identical init script, settings script and build script"() {
root {
'common.gradle'("""
${this.instrument('"common"')}
""")
'init.gradle'('''
apply from: 'common.gradle'
''')
'settings.gradle'('''
apply from: 'common.gradle'
''')
'build.gradle'('''
apply from: 'common.gradle'
''')
}
when:
executer.withArgument('-Iinit.gradle')
run 'help'
then:
def scripts = scriptDetails()
scripts.size() == 3 // same script applied 3 times
scripts.collect { it.className }.unique().size() == 1
scripts.collect { it.classpath }.unique().size() == 1
getCompileBuildFileOperationsCount() == 8 // classpath and body for the common script + identical script x 3 targets
}
@ToBeFixedForConfigurationCache(skip = INVESTIGATE)
def "remapped classes have script origin"() {
root {
'build.gradle'('''
void assertScriptOrigin(Object o, Set seen) {
assert (o instanceof org.gradle.internal.scripts.ScriptOrigin)
// need to get through reflection to bypass the Groovy MOP on closures, which would cause calling the method on the owner instead of the closure itself
def originalClassName = o.class.getMethod('getOriginalClassName').invoke(o)
def contentHash = o.class.getMethod('getContentHash').invoke(o)
assert originalClassName
assert contentHash
println "Action type: ${originalClassName} (remapped name: ${o.class})"
println "Action hash: ${contentHash}"
if (!seen.add(contentHash)) {
throw new AssertionError("Expected a unique hash, but found duplicate: ${o.contentHash} in $seen")
}
}
Set seen = []
assertScriptOrigin(this, seen)
task one {
doLast {
{ ->
assertScriptOrigin(owner, seen) // hack to get a handle on the parent closure
}()
}
}
task two {
def v
v = { assertScriptOrigin(v, seen) }
doFirst(v)
}
task three {
doLast(new Action() {
void execute(Object o) {
assertScriptOrigin(this, seen)
}
})
}
task four {
doLast {
def a = new A()
assertScriptOrigin(a, seen)
}
}
class A {}
''')
}
when:
run 'one', 'two', 'three', 'four'
then:
noExceptionThrown()
}
def "same applied script is compiled once for different projects with different classpath"() {
root {
'common.gradle'('println "poke"')
}
when:
def allCompileOperations = 0
def iterations = 3
def builder = root
iterations.times { n ->
createJarWithProperties("foo${n}.jar", [value: n])
new File(root.baseDir, 'build.gradle').delete()
builder {
'build.gradle'("""
buildscript {
dependencies {
classpath files('foo${n}.jar')
}
}
apply from: 'common.gradle'
""")
}
run 'help'
allCompileOperations += getCompileBuildFileOperationsCount()
}
then:
allCompileOperations == 2 * (1 + iterations) // common + 1 build script per iteration
}
@ToBeFixedForConfigurationCache(because = "test expect script evaluation")
def "script doesn't get recompiled if daemon disappears"() {
root {
buildSrc {
'build.gradle'('''
apply plugin: 'java'
''')
src {
main {
java {
'Foo.java'('public class Foo {}')
}
}
}
}
'build.gradle'(this.simpleBuild('''apply from:'main.gradle' '''))
'main.gradle'(this.simpleBuild('''
task success {
doLast {
println 'ok'
}
}
'''))
}
executer.requireIsolatedDaemons()
executer.requireDaemon()
executer.withGradleUserHomeDir(homeDirectory)
when:
succeeds 'success'
then:
getCompileBuildFileOperationsCount() == 6
when:
def before = scriptDetails()
daemons.daemon.kill()
succeeds 'success'
then:
def scripts = scriptDetails()
scriptsAreReused(before, scripts)
getCompileBuildFileOperationsCount() == 0
}
int getCompileBuildFileOperationsCount() {
return getCompileBuildFileOperationsCount(buildOperations)
}
int getCompileBuildFileOperationsCount(BuildOperationTreeQueries buildOperations) {
return buildOperations.all(CompileScriptBuildOperationType).size()
}
DaemonsFixture getDaemons() {
new DaemonLogsAnalyzer(executer.daemonBaseDir)
}
void hasScript(String path, List scripts) {
assert scripts.find { it.path == path }
}
void scriptHasChanged(String path, List before, List after) {
def script1 = before.find { it.path == path }
def script2 = after.find { it.path == path }
assert script1 != null && script2 != null
assert script1.className == script2.className
assert script1.classpath != script2.classpath
}
void scriptsAreReused(List before, List after) {
assert before.size() == after.size()
for (int i = 0; i < before.size(); i++) {
def script1 = before[i]
def script2 = after[i]
assert script1.path == script2.path
assert script1.className == script2.className
assert script1.classpath == script2.classpath
}
}
void eachScriptIsUnique(List scripts) {
assert scripts.collect { it.path }.unique().size() == scripts.size()
assert scripts.collect { it.className }.unique().size() == scripts.size()
assert scripts.collect { it.classpath }.unique().size() == scripts.size()
}
List scriptDetails(String text = result.output) {
def pattern = Pattern.compile("script=(.*)=(.*),(.*)")
def lines = text.readLines()
def result = []
lines.forEach { line ->
def matcher = pattern.matcher(line)
if (!matcher.matches()) {
return
}
result.add(new ClassDetails(matcher.group(1), matcher.group(2), matcher.group(3)))
}
return result
}
String instrument(String idExpr) {
return """println("script=" + $idExpr + "=" + getClass().name + "," + getClass().classLoader.getURLs().collect { new File(it.toURI()) })"""
}
String simpleBuild(String content = '') {
"""
${content}
${instrument("project.path")}
"""
}
String settings(String... projects) {
createDirs(projects)
String includes = "include ${projects.collect { "'$it'" }.join(', ')}"
"""
${instrument("'settings'")}
$includes
"""
}
String settingsWithBuildScriptsUseProjectName(String... projects) {
createDirs(projects)
String includes = "include ${projects.collect { "'$it'" }.join(', ')}"
"""
${instrument("'settings'")}
$includes
rootProject.children.each { project ->
project.projectDir = new File(project.name)
project.buildFileName = "\${project.name}.gradle"
}
"""
}
String taskThrowingError() {
simpleBuild('''
task someTask() {
doLast {
thisMethodDoesNotExist()
}
}
''')
}
String applyFromRemote(BlockingHttpServer server) {
simpleBuild("""
apply from: '${server.uri}/shared.gradle'
""")
}
class ClassDetails {
final String path
final String className
final String classpath
ClassDetails(String path, String className, String classpath) {
this.path = path
this.className = className
this.classpath = classpath
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy