integrationTest task help - cannot configure dependencies (other tasks and classpath) correctly

Hi. First of all, sorry for the lengthy post, but I strived to be as detailed as possible. I have a two module build with api and core modules. core depends on api - everything works fine in this regard. Now I would like to add an integrationTest task to impl with the following characteristics: 1. check task depends on it, but it runs test task first (here is my problem) 2. I want to be able to only call test, or only integrationTest 3. integrationTest should have the following classpath: runtime (the jars for compile and runtime, and all jars from api module, and the api module itself, and all sourceSets.main classes and resources compiled for impl (here is another problem) 4. there is a sharedTest configuration where I define the jars to use by both test and integrationTest, and it might have common test classes (not yet configured and irrelevant) - this works nice as the test task already works with this layout

I can’t make #2 and $3 work. Here are the relevant excerpts from my build files:

build.gradle in the root project:

subprojects {
    apply plugin: 'groovy'
      repositories {
        mavenCentral()
    }
      sourceSets {
        sharedTest
        integrationTest
    }
      configurations {
        all {
            resolutionStrategy {
                failOnVersionConflict()
            }
            exclude group: 'asm', module: 'asm'
            exclude group: 'junit', module: 'junit'
        }
          sharedTestRuntime.extendsFrom sharedTestCompile
          testCompile.extendsFrom sharedTestCompile
        testRuntime.extendsFrom sharedTestRuntime
          integrationTestCompile.extendsFrom compile, sharedTestCompile
        integrationTestRuntime.extendsFrom runtime, integrationTestCompile, sharedTestRuntime
    }
      dependencies {
        groovy group: 'org.codehaus.groovy', name: 'groovy', version: '2.0.0'
          sharedTestCompile group: 'org.testng', name: 'testng', version: '6.5.2'
        sharedTestCompile group: 'org.hamcrest', name: 'hamcrest-core', version: '1.2.1'
        sharedTestCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.2.1'
          testCompile group: 'org.mockito', name: 'mockito-core', version: '1.9.0', {
            exclude group: 'org.hamcrest', module: 'hamcrest-core'
        }
    }
      task integrationTest(type: Test) {
        description = 'Runs the integration tests.'
        group = 'verification'
        dependsOn integrationTestClasses
        testClassesDir = sourceSets.integrationTest.output.classesDir
        classpath = sourceSets.integrationTest.runtimeClasspath + sourceSets.main.runtimeClasspath
          // if (project.name == 'core') classpath.each { println it } // uncomment to see what is on the classpath - not enough
    }
      tasks.withType(Test) {
        useTestNG()
        testLogging.showStandardStreams = true
    }
      check {
        dependsOn = [test, integrationTest]
    }
}

and build.gradle in module core:

dependencies {
    def neo4jVersion = '1.8.M06'
      compile project(':api')
    compile group: 'org.neo4j', name: 'neo4j-kernel', version: neo4jVersion
    compile group: 'org.neo4j', name: 'neo4j-lucene-index', version: neo4jVersion
      integrationTestCompile group: 'org.neo4j', name: 'neo4j-kernel', version: neo4jVersion, classifier: 'tests'
}

There are the following problems: 1. gradle check -m :api:compileJava SKIPPED :api:compileGroovy SKIPPED :api:processResources SKIPPED :api:classes SKIPPED :api:jar SKIPPED :core:compileJava SKIPPED :core:compileGroovy SKIPPED :core:processResources SKIPPED :core:classes SKIPPED :core:compileIntegrationTestJava SKIPPED :core:compileIntegrationTestGroovy SKIPPED :core:processIntegrationTestResources SKIPPED :core:integrationTestClasses SKIPPED :core:integrationTest SKIPPED :core:compileTestJava SKIPPED :core:compileTestGroovy SKIPPED :core:processTestResources SKIPPED :core:testClasses SKIPPED :core:test SKIPPED :core:check SKIPPED

Gradle tries to call compileIntegrationTest before test, even though check dependsOn = [test, integrationTest] - seems like Gradle is not preserving the ordering I set? (Yes, I tried it without -m to make sure the above is really the task sequence I get in the end.).

  1. gradle integrationTest :api:compileJava UP-TO-DATE :api:compileGroovy UP-TO-DATE :api:processResources UP-TO-DATE :api:classes UP-TO-DATE :api:jar UP-TO-DATE :core:compileJava UP-TO-DATE :core:compileGroovy UP-TO-DATE :core:processResources UP-TO-DATE :core:classes UP-TO-DATE :core:compileIntegrationTestJava UP-TO-DATE :core:compileIntegrationTestGroovy startup failed: /home/wujek/projects/gradletest/core/src/integrationTest/groovy/test/neo4j/Neo4jNetworkTest.groovy: 34: unable to resolve class Neo4jNetwork

and a bunch of other compilation failures. The problem is: neither api.jar nor src/main/java nor src/main/groovy are on the classpath. When I uncomment the line in the integrationTest above to output the classpath, I get this:

/home/wujek/projects/gradletest/core/build/classes/integrationTest /home/wujek/projects/gradletest/core/build/resources/integrationTest /home/wujek/.gradle/caches/artifacts-13/filestore/org.codehaus.groovy/groovy/2.0.0/jar/e83becc219b228e3d0df011cee7c20d7406e9cbe/groovy-2.0.0.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.testng/testng/6.5.2/jar/5069d2e72356ed87645c5fc3622b0bbb5e0667c9/testng-6.5.2.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.hamcrest/hamcrest-core/1.2.1/jar/e89706d7a0641823a7d3f20c2b96272f622d155c/hamcrest-core-1.2.1.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.hamcrest/hamcrest-library/1.2.1/jar/b54856422a8b58f0fe61b3a762d024f5e6d0556d/hamcrest-library-1.2.1.jar /home/wujek/.gradle/caches/artifacts-13/filestore/antlr/antlr/2.7.7/jar/83cd2cd674a217ade95a4bb83a8a14f351f48bd0/antlr-2.7.7.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.ow2.asm/asm/4.0/jar/659add6efc75a4715d738e73f07505246edf4d66/asm-4.0.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.ow2.asm/asm-tree/4.0/jar/67bd266cd17adcee486b76952ece4cc85fe248b8/asm-tree-4.0.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.ow2.asm/asm-commons/4.0/jar/a839ec6737d2b5ba7d1878e1a596b8f58aa545d9/asm-commons-4.0.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.ow2.asm/asm-util/4.0/jar/d7a65f54cda284f9706a750c23d64830bb740c39/asm-util-4.0.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.ow2.asm/asm-analysis/4.0/jar/1c45d52b6f6c638db13cf3ac12adeb56b254cdd7/asm-analysis-4.0.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.beanshell/bsh/2.0b4/jar/a05f0a0feefa8d8467ac80e16e7de071489f0d9c/bsh-2.0b4.jar /home/wujek/.gradle/caches/artifacts-13/filestore/com.beust/jcommander/1.12/jar/7409692b48022f9eca7445861defbcdb9ee3c2a8/jcommander-1.12.jar /home/wujek/.gradle/caches/artifacts-13/filestore/org.yaml/snakeyaml/1.6/jar/a1e23e31c424d566ee27382e373d73a28fdabd88/snakeyaml-1.6.jar /home/wujek/projects/gradletest/core/build/classes/main /home/wujek/projects/gradletest/core/build/resources/main

Neither api.jar, nor neo4j-kernel-test.jar are on the classpath (although neo4j actually is there in the end, as I don’t get any compilation errors for its classes). The core module main classes seem to be on the classpath, but I get compilation errors saying they are not available (in the previous snippet).

Could I get some help with these 2 problems? I am obviously misconfiguring the classpath for the integrationTest task, and the unordered / alphabetically sorted dependency list seems to be a gradle problem?

wujek

So I found a gradle issue to the dependsOn ordering problem and added my comment. It is very surprising that there are so many people who get burned by this, and yet nothing is done. And the fix would probably be to use a LinkedHashSet instead of the TreeSet in the code (the patch mentioned there uses an ArrayList, but that doesn’t take care of duplicates, as LinkedHashSet would do). So just the one problem of incomplete classpath remains unsolved ;d

wujek

So the not-complete classpath being printed out was because I put the printing in the root script, and not in the core/build.gradle script. So now the output (the most relevant parts) show:

/home/wujek/projects/gradletest/core/build/classes/integrationTest /home/wujek/projects/gradletest/core/build/resources/integrationTest /home/wujek/projects/gradletest/api/build/libs/api-0.0.1-SNAPSHOT.jar … a bunch of jars, including all mockitos and tesng and neo4j /home/wujek/projects/gradletest/core/build/classes/main /home/wujek/projects/gradletest/core/build/resources/main

So both api.jar and classes/main are there (and are not empty), but exactly these classes cannot be found. I’m either doing something stupid here, or missing something obvious.

wujek

Ok, so I have been setting the cp for the integrationTest task the whole time, and the compileIntegrationTest tasks were unconfigured the whole time… As I said, stupid. I will try to fix that tomorrow, it is rather late already, hopefully I get it to work. Thanks for listening ;d

wujek

Using an ArrayList or LinkedHashSet wouldn’t change anything. Besides, ‘foo dependsOn bar, baz’ is simply a shorthand way of saying ‘foo dependsOn bar; foo dependsOn baz’ in Gradle. It isn’t (and never was) meant to say anything about the order between bar and baz. We are hesitant to introduce Ant’s concept of soft dependencies because we think that it has its own problems. We do think that some abstraction is missing, but we haven’t found the right solution yet.

dependsOn bar, baz or dependsOn bar dependsOn baz

seems to be very clear about what the ordering is - all things equal (no interdependencies), my (the build developer’s) preferences should be taken into account. Agreed, if you feel this is wrong, you of course can, but this kind of breaks the principle of least astonishment, and there still is no ‘right’ way to do it. Also. some of the proposals in one of the threads (by Adam Murdoch, I think) just won’t work (hardcoding ‘clean’ to be called as the very first thing, the descructor tasks - don’t think they apply to this case at all, adding an ‘orderedDependsOn’ just adds yet another property to the mix, which will make the API more confusing and my bet is nobody will use the unordered dependsOn (but I have been wrong before ;d). I’m really curious when and how this functionality will be available. I’m also curious what problems does preserving the ordering have - everyone mentions them. but nobody gives any examples. Is it simply because the code to create the DAG and preserve the ordering is harder to write? Anyways, there are a few workarounds, I think I will use the one that forces the alphabetic sorting to be the way I want it to… How nice is that.

I disagree about the least astonishment. I would be very surprised if foo.dependsOn bar, baz said anything about the order between bar and baz. It’s just that people are used to it from Ant. As I’ve already tried to explain, this is not at all an implementation problem. It’s a problem of design and finding the right abstractions. Two problems with Ant’s soft dependencies are 1. they are not guaranteed (i.e. you always have to worry that the order isn’t what you want, this can change anytime you add a new task dependency somewhere in your build, and there is no way to detect this other than observing that the order of execution has changed) 2. they tend to be overused, unnecessarily enforcing an order of execution, which takes away freedom for the build system to make decisions on its own.

OK, here is my final config, if anybody is interested:

subprojects {
    repositories {
        mavenCentral()
    }
      apply {
        plugin 'groovy'
    }
      sourceSets {
        sharedTest
        integrationTest
    }
      configurations {
        testCompile.extendsFrom sharedTestCompile
        testRuntime.extendsFrom sharedTestRuntime
          integrationTestCompile.extendsFrom compile, sharedTestCompile
        integrationTestRuntime.extendsFrom runtime, sharedTestRuntime
    }
      dependencies {
        groovy group: 'org.codehaus.groovy', name: 'groovy', version: '2.0.0'
          sharedTestCompile group: 'org.testng', name: 'testng', version: '6.5.2'
        sharedTestCompile group: 'org.hamcrest', name: 'hamcrest-core', version: '1.2.1'
        sharedTestCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.2.1'
          testCompile group: 'org.mockito', name: 'mockito-core', version: '1.9.0', {
            exclude group: 'org.hamcrest', module: 'hamcrest-core'
        }
    }
      compileIntegrationTestJava {
        classpath += sourceSets.main.output
    }
      compileIntegrationTestGroovy {
        classpath = compileIntegrationTestJava.classpath
    }
      task integrationTest(type: Test) {
        description = 'Runs the integration tests.'
        group = 'verification'
        dependsOn integrationTestClasses
        testClassesDir = sourceSets.integrationTest.output.classesDir
        classpath = sourceSets.integrationTest.runtimeClasspath + sourceSets.main.output
    }
      tasks.withType(Test) {
        useTestNG()
        testLogging.showStandardStreams = true
    }
      check {
        dependsOn integrationTest
    }
}

In the core project, I have:

dependencies {
    def neo4jVersion = '1.8.M06'
      compile project(':api')
    compile group: 'org.neo4j', name: 'neo4j-kernel', version: neo4jVersion
    compile group: 'org.neo4j', name: 'neo4j-lucene-index', version: neo4jVersion
      integrationTestCompile group: 'org.neo4j', name: 'neo4j-kernel', version: neo4jVersion, classifier: 'tests'
}

Could anybody tell me if this can be done any easier, especially the classpath handling for the various compilation / integrationTest tasks?

The integrationTest task should be:

task integrationTest(type: Test) {
        description = 'Runs the integration tests.'
        group = 'verification'
        dependsOn integrationTestClasses
        classpath = sourceSets.integrationTest.runtimeClasspath + sourceSets.main.output
        testClassesDir = sourceSets.integrationTest.output.classesDir
        testReportDir = new File("$project.reporting.baseDir", 'integrationTests')
        testResultsDir = new File("$project.buildDir", 'integrationTest-results')
        testSrcDirs = (sourceSets.integrationTest.java.srcDirs + sourceSets.integrationTest.groovy.srcDirs).asList()
    }

This way, it uses its own report directories and so on, and it doesn’t mess up the test tasks (or the test task doesn’t mess up integrationTest’s) up-to-date status.

wujek