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