James Daugherty
03/12/2025, 8:15 PMtestImplementation 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?Vampire
03/12/2025, 9:55 PMTest
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.James Daugherty
03/12/2025, 11:14 PMJames Daugherty
03/12/2025, 11:14 PMJames Daugherty
03/12/2025, 11:15 PMJames Daugherty
03/12/2025, 11:15 PMVampire
03/13/2025, 4:38 PMfinding the dependencyIf 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".
James Daugherty
03/13/2025, 5:51 PMJames Daugherty
03/13/2025, 5:51 PMJames Daugherty
03/13/2025, 5:51 PMVampire
03/13/2025, 10:45 PMFileCollection
, so ... yes 🙂Vampire
03/13/2025, 10:45 PM.singleFile
for exampleVampire
03/13/2025, 10:49 PMCopy
, 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.James Daugherty
03/13/2025, 11:40 PMJames Daugherty
03/14/2025, 1:17 AM```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)
}
}
}```
James Daugherty
03/14/2025, 1:17 AMJames Daugherty
03/14/2025, 1:18 AMJames Daugherty
03/14/2025, 1:19 AMJames Daugherty
03/14/2025, 1:20 AMJames Daugherty
03/14/2025, 1:25 AM```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)
}```
James Daugherty
03/14/2025, 1:25 AMJames Daugherty
03/14/2025, 1:26 AM```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)
}```
James Daugherty
03/14/2025, 1:28 AMJames Daugherty
03/14/2025, 1:29 AM```tasks.register('extractTckJar', Sync) {
dependsOn(configurations.tck)
from { zipTree(configurations.tck.singleFile) }
into(layout.buildDirectory.dir('extracted-tck-classes'))
}```
James Daugherty
03/14/2025, 1:38 AMJames Daugherty
03/14/2025, 1:53 AM```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'))
}```
Vampire
03/14/2025, 3:21 AMSo If I change the tck project to use compileOnly, then only the files I compile would be in that configurationYou 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:
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.tasks.register('extractTckJar', Sync) {
That's still lazy, yes?Of course not, why should it be?
Or if there were multiple files, something like thisNot 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:
Oh, thanks for reminding me. I totally forgot that you can give a closure toCopy codefrom { zipTree(configurations.tck.singleFile) }
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.
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 likeCopy codedependsOn('extractTckJar')
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.James Daugherty
03/14/2025, 9:52 AMJames Daugherty
03/14/2025, 1:24 PMJames Daugherty
03/14/2025, 1:25 PM```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)
}```
James Daugherty
03/14/2025, 1:25 PMJames Daugherty
03/14/2025, 1:27 PMVampire
03/14/2025, 1:47 PMWhat I still don't understand is how you would declare an artifact transformJust 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:
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.
James Daugherty
03/15/2025, 12:50 PMfrom(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())
}
}
}```
James Daugherty
03/15/2025, 12:51 PMJames Daugherty
03/15/2025, 12:51 PMVampire
03/15/2025, 5:45 PMcopy
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.James Daugherty
03/17/2025, 8:15 PM