Hi Everyone, I am not sure if this is a gradle or...
# community-support
j
Hi Everyone, I am not sure if this is a gradle or a junit 5 question, any help would be appreciated: Does anyone know how to run tests from an external, published gradle project? For more information: I have a 'tck' project that's meant to test implementations of my base project. The tck project has it's tests packaged into a jar file so that another project can use them to run against their implementation. I've successfully included them in an example implementation's test classpath via this code:
Copy code
testImplementation project(path: ':tck', configuration: 'testArtifacts')
The tests however aren't run from the jar file. I know the jar is on the classpath since I can output the classpath from a test in that same project. I also noticed an old pull request that references won't fix tickets: https://github.com/gradle/gradle/pull/194 ... but it's so old wasn't sure if anyone knows a modern way to do this. If this isn't a feature of the Test task in gradle, can the jar file be exploded so junit finds the tests? Does anyone know of a way to have the test task or junit find tests inside of a jar on the classpath?
v
I don't know for sure, never had that use-case so far. But looking at the JavaDoc of
Test
I'd guess you have two options, where as you suggested unpacking the jar and then configuring
testClassesDirs
to that directory, maybe through playing with includes and
scanForTestClasses
. Maybe it is enough to set an included, maybe you need an included and disable that option. Have a look at their docs.
j
That was actually my solution, finding the dependency and extracting the jar , then adding the extracted location to the testClassesDir
I haven't found any other solution.
I don't think scanForTestClasses will work because I don't see any code that handles jar file scanning. Interesting enough, there are changes on the junit side to handle scanning in jar file, but only if the runner is in the jar file too.
Thank you @Vampire
👌 1
v
finding the dependency
If going that way, I would just use a separate configuration where only that jar non-transitively is contained, then you do not need any "finding".
if you have any suggestions they are most welcome!
Is there a way to fetch all files for a configuration?
v
A configuraton is a
FileCollection
, so ... yes 🙂
If there for example is only a single file in it you can just use
.singleFile
for example
But of course better not at configuration time, but execution time. Doing configuration resolution at configuration time like you do there has many drawbacks and should be avoided. So better either use an artifact transform to do the unarchiving, or if you insist, at least do not use a task of type
Copy
, but a
copy { ... }
in a
doFirst { ... }
or
doLast { ... }
action, so that the resolving is not done too early but you can still use
zipTree
. Also calling
resolve()
manually on a configuration is in 99.87 % of the cases never what you should do. And consider never using
Copy
or
copy { ... }
. In 98.7 % of the cases you want
Sync
or
sync { ... }
instead to not risk stale files lying around.
j
Thank you
👌 1
So If I change the tck project to use compileOnly, then only the files I compile would be in that configuration . This works:
```configurations {
tck
}
dependencies {
if (!project.hasProperty('includeBaseTckClass') || project.findProperty('includeBaseTckClass')) {
testImplementation project(':grails-datastore-gorm-tck-base')
}
tck project(':grails-datastore-gorm-tck')
testImplementation project(':grails-datastore-gorm-tck-domains')
}
// TODO: gradle will happily put the jar file on the classpath, but junit won't find the tests. Dynamic test discovery also won't find them.
tasks.register('extractTckJar') {
dependsOn(configurations.tck)
Provider<Directory> extractTarget = layout.buildDirectory.dir('extracted-tck-classes')
outputs.dir(extractTarget)
doFirst {
extractTarget.get().asFile.deleteDir()
}
doLast {
copy {
from(zipTree(configurations.tck.singleFile))
into(extractTarget)
}
}
}```
But looking at your comment I have a few questions ...
1. if I were to have multiple jars , you said it's a file collection, so I can just do a findAll { it.name } to find the file?
2. trying to switch copy to sync errors, with: Could not find method from() for arguments [ZIP 'jarFile.jar' ] on task extractTaskJar
It seems to always want a source path, am I missing something about the sync api?
Ah, I guess I can do this instead:
```tasks.register('extractTckJar', Sync) {
dependsOn(configurations.tck)
Provider<Directory> extractTarget = layout.buildDirectory.dir('extracted-tck-classes')
outputs.dir(extractTarget)
from(zipTree(configurations.tck.singleFile))
into(extractTarget)
}```
That's still lazy, yes?
Or if there were multiple files, something like this:
```tasks.register('extractTckJar', Sync) {
dependsOn(configurations.tck)
Provider<Directory> extractTarget = layout.buildDirectory.dir('extracted-tck-classes')
outputs.dir(extractTarget)
from(zipTree(configurations.tck.find { File file -> file.name.startsWith("grails-datastore-gorm-tck-${projectVersion}") }))
into(extractTarget)
}```
Do I even need the outputs in that example?
I guess this would be better:
```tasks.register('extractTckJar', Sync) {
dependsOn(configurations.tck)
from { zipTree(configurations.tck.singleFile) }
into(layout.buildDirectory.dir('extracted-tck-classes'))
}```
Hrm, running the task syncs it correctly, but unfortunately the compileTestGroovy now fails because it can't find the files. Even though they're added to the testClasspath 👀
Ah , it's because of the configuration:
```configurations {
tck
}
dependencies {
if (!project.hasProperty('includeBaseTckClass') || project.findProperty('includeBaseTckClass')) {
testImplementation project(':grails-datastore-gorm-tck-base')
}
// So we can easily extract the compiled classes
tck project(':grails-datastore-gorm-tck')
// Because we reference tests in the SimpleTestSuite to be able to run them in the IDE, they need explicitly added to the classpath
testImplementation project(':grails-datastore-gorm-tck')
testImplementation project(':grails-datastore-gorm-tck-domains')
}
// TODO: gradle will happily put the jar file on the classpath, but junit won't find the tests.
// Dynamic test discovery also won't find them so extract the class files to force junit to work.
tasks.register('extractTckJar', Sync) {
dependsOn(configurations.tck)
from { zipTree(configurations.tck.singleFile) }
into(layout.buildDirectory.dir('extracted-tck-classes'))
}
tasks.withType(Test).configureEach {
dependsOn('extractTckJar')
testClassesDirs += files(layout.buildDirectory.dir('extracted-tck-classes'))
}```
v
So If I change the tck project to use compileOnly, then only the files I compile would be in that configuration
You can just on consumer side say "transitive false" to only get the dependency but not its transitive dependencies. You can say that at the dependency declaration or on the resolvable configuration you use to resolve it.
```doFirst {
extractTarget.get().asFile.deleteDir()
}
doLast {
copy {
from(zipTree(configurations.tck.singleFile))
into(extractTarget)
}
}```
Why do you split this into two actions? Besides that, if you use
sync { ... }
, the first one is obsolete anyway and actually just needlessly wastes time.
1. if I were to have multiple jars , you said it's a file collection, so I can just do a findAll { it.name } to find the file?
Why should you have multiple files if you make sure there is only one file? If there are multiple files, the
.singleFile
will fail with exception. A configuration of course could be multiple files in general as you can declare multiple dependencies and if you don't make it non-transitive each dependency could bring in further dependencies. But if you make sure there is only one dependency and you use non-transitive and that dependency has an artifact, then you will have exactly one file.
trying to switch copy to sync errors, with: Could not find method from() for arguments [ZIP 'jarFile.jar' ] on task extractTaskJar
copy
and
sync
are identical in possibilities and api, the only difference is, that sync in the end deletes files it did not copy to the target, so makes the manual up-front delete unnecessary. If
sync
did not work,
copy
did not work. Or you changed something more. Or you somehow used a wrong
sync
.
Ah, I guess I can do this instead:
tasks.register('extractTckJar', Sync) {
Of course you can, but you surely shouldn't, for the same reasons I told you before. You resolve the configuration at configuration time which is bad.
That's still lazy, yes?
Of course not, why should it be?
Or if there were multiple files, something like this
Not worse, but as bad as the single-file version. And again, why should there ever be multiple files if you ensure there is only one? Don't catch errors that cannot happen.
Do I even need the outputs in that example?
No, if you use the
Sync
(or
Copy
) task, you just re-declare what is already task output. But as you should not use the
Sync
task but the
sync { ... }
function you will need it. Of course only until you finally switch to an artifact transform because at that point you will switch back to a
Sync
task and then also the manual input declaration will become superfluous. Which makes me recognize, that you did not declare the inputs of the task but instead a manual
dependsOn
. Never declare a manual
dependsOn
unless the left-hand task is a lifecycle task. Declare the configuration as
inputs.files(...)
(in the function-version especially) or up-to-date checks are not working as expected.
I guess this would be better:
Copy code
from { zipTree(configurations.tck.singleFile) }
Oh, thanks for reminding me. I totally forgot that you can give a closure to
from
. That indeed should be lazy enough. But still, better declare the tck configuration as input files, not
dependsOn
. As mentioned, any manual
dependsOn
that does not have a lifecycle task on the left-hand side is a code-smell and sign of something being done wrongly, even if in this specific case it should not make much difference.
Copy code
dependsOn('extractTckJar')
What I just said. It hints at you not assigning the testClassesDirs correctly, or it would carry the necessary task dependency automatically. But configuring paths manually and then task dependencies manually is exactly what you should never do. Instead you should wire task outputs to task inputs. For example in Kotlin DSL it could look like
Copy code
testClassesDirs = objects.fileCollection().from(extractTckJar)
(with
extractTckJar
being a variable holding the
TaskProvider
from registering it or looking it up) Btw. in case I didn't do it yet, I strongly recommend switching to Kotlin DSL. By now it is the default DSL, you immediately get type-safe build scripts, actually helpful error messages if you mess up the syntax, and amazingly better IDE support if you use a good IDE like IntelliJ IDEA or Android Studio.
j
Thank you this was extremely helpful!
👌 1
What I still don't understand is how you would declare an artifact transform - zipTree() does not return file paths. sync { } in the doLast / doFirst appears to only take source paths.
I believe this will work (but it doesn't move the sync to doLast/doFirst as you suggest - it's still lazy only because of the from{}) :
```configurations {
tck {
transitive = false
}
}
dependencies {
if (!project.hasProperty('includeBaseTckClass') || project.findProperty('includeBaseTckClass')) {
testImplementation project(':grails-datastore-gorm-tck-base')
}
// So we can easily extract the compiled classes
tck project(':grails-datastore-gorm-tck')
// Because we reference tests in the SimpleTestSuite to be able to run them in the IDE,
// the tests need explicitly added to the classpath as well
testImplementation project(':grails-datastore-gorm-tck')
testImplementation project(':grails-datastore-gorm-tck-domains')
runtimeOnly 'org.apache.groovy:groovy-dateutil', {
// Groovy Date Utils Extensions are used in the tests
}
}
// TODO: gradle will happily put the jar file on the classpath, but junit won't find the tests.
// Dynamic test discovery also won't find them so extract the class files to force junit to work.
TaskProvider<Sync> extractTck = tasks.register('extractTckJar', Sync) {
inputs.files(configurations.tck)
from { zipTree(configurations.tck.singleFile) }
into(layout.buildDirectory.dir('extracted-tck-classes'))
}
tasks.withType(Test).configureEach {
testClassesDirs += objects.fileCollection().from(extractTckJar)
}```
And as for the kotlin dsl, I'll discuss with my team mates, but at this time I don't think I can adopt it for this project.
And to state it again, I really appreciate the education. Thank you so much!
v
What I still don't understand is how you would declare an artifact transform
Just read the documentation about artifact transforms? A transform that unpacks the artifacts is exactly the example that is used to explain it as far as I remember. In a company project I even pimped that to specify how many top-level directories I want to have stripped.
zipTree() does not return file paths.
I have no idea what you want to say with this, or how it is related to artifact transforms. If you use an artifact transform you are not using zipTree.
sync { } in the doLast / doFirst appears to only take source paths.
"Appears" might be the right word. As I said,
Copy
and
Sync
are interchangeable,
copy
and
sync
are interchangeable. They take the identical inputs and have the same possibilities. The only difference is, that `Sync`/`sync` do delete the files they did not place in the destination while `Copy`/`copy` do not. If you see something working with
copy
but not with
sync
, you are either doing something wrong or hit a very strange bug I've never seen before. Feel free to show me an MCVE that proofs what you say. I just 2 minutes ago re-tried it and it works just like expected with this:
Copy code
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.10") { isTransitive = false }
}
val foo by tasks.registering {
    doLast {
        sync {
            from(zipTree(configurations.compileClasspath.get().singleFile))
            into(layout.buildDirectory.dir("foo"))
        }
    }
}
But as we found out you don't need the
sync
but can use the
Sync
with
from { ... }
, so more depends on your curiosity whether you try further.
I believe this will work (but it doesn't move the sync to doLast/doFirst as you suggest - it's still lazy only because of the from{}) :
One doubt you have to check and one improvement suggestion from a cursory look. I'm not sure nowadays what the
+=
in
testClassesDirs += objects.fileCollection().from(extractTckJar)
do exactly in Groovy DSL and whether it will properly preserve the task dependency. So double-check If you execute
./gradlew test
whether the extract task is automatically executed first. If not, this would probably work better:
testClassesDirs = objects.fileCollection().from(testClassesDirs, extractTckJar)
About the improvement, you can make the
testImplementation
configuration
extendsFrom
the
tck
configuration. Then anything you declare for
tck
is automatically in
testImplementation
too, so you only need to declare the dependency once.
And to state it again, I really appreciate the education. Thank you so much!
Don't worry, that's the feeling I got, otherwise I probably would have stopped responding. 😄 I'm just a user like you, you know, not in any way affiliated to Gradle.
j
FYI: I think the delegate is wrong for the sync task in a doLast { }.
from(zipTree(configurations.tck.singleFile))
will fail in groovy if defined like so:
```tasks.register('extractTckJar') {
inputs.files(configurations.tck)
doLast {
sync {
from(zipTree(configurations.tck.singleFile))
into(layout.buildDirectory.dir('extracted-tck-classes').get())
}
}
}```
but will pass if defined like this:
```tasks.register('extractTckJar') {
inputs.files(configurations.tck)
doLast {
project.sync {
from(zipTree(configurations.tck.singleFile))
into(layout.buildDirectory.dir('extracted-tck-classes').get())
}
}
}```
this issue doesn't exist with the copy { } , and only with sync { }
I'm guessing kotlin's resolution doesn't have this, which is why your example works
v
Oh, sorry, never seen
copy
and
sync
behave differently. I would even say you should report that as bug to Gradle. It seems for
Project#sync
Gradle magic does set the
delegate
properly, while for
DefaultScript#sync
(which is the one you used when not using
project.
) this special treatment is missing.
👍 1
j