org.gradle.language.AbstractNativeLanguageIncrementalCompileIntegrationTest.groovy Maven / Gradle / Ivy
/*
* Copyright 2013 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.language
import groovy.io.FileType
import groovy.transform.NotYetImplemented
import org.gradle.integtests.fixtures.CompilationOutputsFixture
import org.gradle.nativeplatform.fixtures.AbstractInstalledToolChainIntegrationSpec
import org.gradle.nativeplatform.fixtures.app.IncrementalHelloWorldApp
import org.gradle.test.fixtures.file.TestFile
import org.gradle.util.GUtil
import spock.lang.Unroll
abstract class AbstractNativeLanguageIncrementalCompileIntegrationTest extends AbstractInstalledToolChainIntegrationSpec {
IncrementalHelloWorldApp app
String compileTask
TestFile sourceFile
TestFile sharedHeaderFile
TestFile commonHeaderFile
TestFile otherHeaderFile
List otherSourceFiles = []
TestFile objectFileDir
CompilationOutputsFixture outputs
abstract IncrementalHelloWorldApp getHelloWorldApp();
String getSourceType() {
GUtil.toCamelCase(app.sourceType)
}
def "setup"() {
app = getHelloWorldApp()
compileTask = ":compileMainExecutableMain${sourceType}"
buildFile << app.pluginScript
buildFile << app.extraConfiguration
buildFile << """
model {
components {
main(NativeExecutableSpec)
}
}
"""
and:
sourceFile = app.mainSource.writeToDir(file("src/main"))
sharedHeaderFile = app.libraryHeader.writeToDir(file("src/main"))
commonHeaderFile = app.commonHeader.writeToDir(file("src/main"))
app.librarySources.each {
otherSourceFiles << it.writeToDir(file("src/main"))
}
otherHeaderFile = file("src/main/headers/other.h") << """
// Dummy header file
"""
objectFileDir = file("build/objs/main")
outputs = new CompilationOutputsFixture(objectFileDir)
}
def "recompiles changed source file only"() {
given:
outputs.snapshot { run "mainExecutable" }
when:
sourceFile << """
// Changed source file
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
}
def "recompiles all source files that include changed header file"() {
given:
outputs.snapshot { run "mainExecutable" }
when:
sharedHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFiles allSources
}
def "recompiles only source file that includes changed header file"() {
given:
sourceFile << """
#include "${otherHeaderFile.name}"
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
otherHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
}
def "does not recompile when fallback mechanism is used and there are empty directories"() {
given:
file("src/main/headers/empty/directory").mkdirs()
sourceFile << """
#define MY_HEADER "${otherHeaderFile.name}"
#include MY_HEADER
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
run "mainExecutable"
then:
skipped compileTask
}
def "does not recompile when included header has the same name as a directory"() {
given:
buildFile << """
model {
components {
main {
sources.all {
exportedHeaders {
srcDirs = [ "src/other", "src/main/headers" ]
}
}
}
}
}
"""
// This is a directory named 'directory'
file("src/other/directory").mkdirs()
// This is a header named 'directory'
file("src/main/headers/directory") << '#pragma message("including directory named header")'
file("src/main/headers/macro.h") << '#pragma message("including macro header")'
sourceFile << """
#include "directory"
#define MACRO "macro.h"
#include MACRO
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
run "mainExecutable"
then:
executed compileTask
skipped compileTask
}
@NotYetImplemented
def "recompiles when included header has the same name as a directory and the directory becomes a file"() {
given:
buildFile << """
model {
components {
main {
sources.all {
exportedHeaders {
srcDirs = [ "src/other", "src/main/headers" ]
}
}
}
}
}
"""
// directory header starts out as a directory
def directoryHeader = file("src/other/directory")
directoryHeader.mkdirs()
// this is the a header file named 'directory'
file("src/main/headers/directory") << '#pragma message("including directory named header")'
sourceFile << """
#include "directory"
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
directoryHeader.deleteDir()
directoryHeader << '#pragma message("NEW directory named header")'
and:
executer.withArgument("--info")
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
result.assertOutputContains("NEW directory named header")
}
def "source is always recompiled if it includes header via macro"() {
given:
def notIncluded = file("src/main/headers/notIncluded.h")
notIncluded.text = """#pragma message("should not be used")"""
sourceFile << """
#define MY_HEADER "${otherHeaderFile.name}"
#include MY_HEADER
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
otherHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
when: "Header that is NOT included is changed"
notIncluded << """
// Dummy header file
"""
and:
run "mainExecutable"
then: "Source is still recompiled"
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
}
def "source is not recompiled when preprocessor removed header is changed"() {
given:
def notIncluded = file("src/main/headers/notIncluded.h")
notIncluded.text = """#pragma message("should not be used")"""
sourceFile << """
#if 0
#include "${notIncluded.name}"
#else
#include "${otherHeaderFile.name}"
#endif
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
otherHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
and:
!output.contains("should not be used")
when:
// this header isn't included
notIncluded << """
// Dummy header file
"""
and:
run "mainExecutable"
then:
// TODO: This is inefficient behavior, we should skip the compile task because 'notIncluded' is not used.
// skipped compileTask
executedAndNotSkipped compileTask
}
def "source is compiled when preprocessor removed header does not exist"() {
given:
sourceFile << """
#if 0
#include "doesNotExist.h"
#else
#include "${otherHeaderFile.name}"
#endif
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
otherHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
when:
run "mainExecutable"
then:
skipped compileTask
}
def "recompiles source file when transitively included header file is changed"() {
given:
def transitiveHeaderFile = file("src/main/headers/transitive.h") << """
// Dummy header file
"""
otherHeaderFile << """
#include "${transitiveHeaderFile.name}"
"""
sourceFile << """
#include "${otherHeaderFile.name}"
"""
and:
outputs.snapshot { run "mainExecutable" }
when:
transitiveHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
}
def "recompiles source file when an included header file is renamed"() {
given:
outputs.snapshot { run "mainExecutable" }
and:
final newFile = file("src/main/headers/changed.h")
newFile << sharedHeaderFile.text
sharedHeaderFile.delete()
when:
fails "mainExecutable"
then:
executedAndNotSkipped compileTask
failure.assertHasDescription("Execution failed for task '${compileTask}'.");
}
def "does not recompile any sources when unused header file is changed"() {
given:
outputs.snapshot { run "mainExecutable" }
when:
otherHeaderFile << """
// Some extra content
"""
and:
run "mainExecutable"
then:
executed compileTask
skipped compileTask
and:
outputs.noneRecompiled()
}
// We can't implement this because we rebuild everything when the include path changes
@NotYetImplemented
@Unroll
def "does not recompile when include path has #testCase"() {
given:
outputs.snapshot { run "mainExecutable" }
file("src/additional-headers/other.h") << """
// extra header file that is not included in source
"""
file("src/replacement-headers/${sharedHeaderFile.name}") << """
// replacement header file that is included in source
"""
when:
buildFile << """
model {
components {
main {
sources {
${app.sourceType} {
exportedHeaders {
srcDirs ${headerDirs}
}
}
}
}
}
}
"""
and:
run "mainExecutable"
then:
executed compileTask
skipped compileTask
and:
outputs.noneRecompiled()
where:
testCase | headerDirs
"extra header dir after" | '"src/main/headers", "src/additional-headers"'
"extra header dir before" | '"src/additional-headers", "src/main/headers"'
"replacement header dir after" | '"src/main/headers", "src/replacement-headers"'
}
def "recompiles when include path is changed so that replacement header file occurs before previous header"() {
given:
outputs.snapshot { run "mainExecutable" }
file("src/replacement-headers/${sharedHeaderFile.name}") << sharedHeaderFile.text
file("src/replacement-headers/${commonHeaderFile.name}") << commonHeaderFile.text
when:
buildFile << """
model {
components {
main {
sources {
${app.sourceType} {
exportedHeaders {
srcDirs "src/replacement-headers", "src/main/headers"
}
}
}
}
}
}
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFiles allSources
}
def "recompiles when replacement header file is added before previous header to existing include path"() {
given:
buildFile << """
model {
components {
main {
sources {
${app.sourceType} {
exportedHeaders {
srcDirs "src/replacement-headers", "src/main/headers"
}
}
}
}
}
}
"""
outputs.snapshot { run "mainExecutable" }
when:
file("src/replacement-headers/${sharedHeaderFile.name}") << sharedHeaderFile.text
file("src/replacement-headers/${commonHeaderFile.name}") << commonHeaderFile.text
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFiles allSources
}
def "recompiles when replacement header file is added to source directory"() {
given:
outputs.snapshot { run "mainExecutable" }
when:
sourceFile.parentFile.file(sharedHeaderFile.name) << sharedHeaderFile.text
sourceFile.parentFile.file(commonHeaderFile.name) << commonHeaderFile.text
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFiles allSources + [commonHeaderFile]
}
def "recompiles all source files and removes stale outputs when compiler arg changes"() {
given:
def extraSource = file("src/main/${app.sourceType}/extra.${app.sourceExtension}")
extraSource << sourceFile.text.replaceAll("main", "main2")
outputs.snapshot { run "mainExecutable" }
objectFileFor(extraSource).assertExists()
when:
sourceFile << """
// Changed source file
"""
buildFile << """
model {
components {
main {
binaries.all {
${helloWorldApp.compilerDefine("MY_DEF")}
}
}
}
}
"""
extraSource.delete()
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFiles allSources
objectFileFor(extraSource).assertDoesNotExist()
}
def "recompiles all source files when generated object files are removed"() {
given:
outputs.snapshot { run "mainExecutable" }
when:
objectFileDir.deleteDir()
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFiles allSources
}
def "removes output file when source file is renamed"() {
given:
outputs.snapshot { run "mainExecutable" }
when:
final newFile = file("src/main/${app.sourceType}/changed.${app.sourceExtension}")
newFile << sourceFile.text
sourceFile.delete()
and:
run "mainExecutable"
then:
outputs.recompiledFile newFile
objectFileFor(sourceFile).assertDoesNotExist()
}
def "removes output file when source file is removed"() {
given:
def extraSource = file("src/main/${app.sourceType}/extra.${app.sourceExtension}")
extraSource << sourceFile.text.replaceAll("main", "main2")
outputs.snapshot { run "mainExecutable" }
and:
objectFileFor(extraSource).assertExists()
when:
extraSource.delete()
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
objectFileFor(extraSource).assertDoesNotExist()
outputs.noneRecompiled()
}
def "removes output files when all source files are removed"() {
given:
run "mainExecutable"
def executable = executable("build/exe/main/main")
executable.assertExists()
when:
file("src/main").eachFileRecurse(FileType.FILES) {
println "deleting ${it}"
it.delete()
}
and:
run "mainExecutable"
then: "linker output file is removed"
executable.assertDoesNotExist()
// Stale object files are removed when a new file is added to the source set
when:
def newSource = file("src/main/${app.sourceType}/newfile.${app.sourceExtension}") << """
#include
int main () {
printf("hello");
return 0;
}
"""
and:
run "mainExecutable"
then:
executable.exec().out == "hello"
objectFileFor(newSource).assertExists()
and: "Previous object files are removed"
objectFileFor(sourceFile).assertDoesNotExist()
otherSourceFiles.each {
objectFileFor(it).assertDoesNotExist()
}
}
def "incremental compile is not effected by other compile tasks"() {
given:
buildFile << """
model {
components {
other(NativeExecutableSpec)
}
}
"""
app.writeSources(file("src/other"))
app.commonHeader.writeToDir(file("src/other"))
and:
outputs.snapshot { run "mainExecutable" }
and:
// Completely independent compile task (state should be independent)
run "otherExecutable"
when:
sourceFile << """
// Changed source file
"""
and:
run "mainExecutable"
then:
executedAndNotSkipped compileTask
and:
outputs.recompiledFile sourceFile
}
List getAllSources() {
return [sourceFile] + otherSourceFiles
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy