How does Kotest discover tests? Does it scan entir...
# kotest
w
How does Kotest discover tests? Does it scan entire classpath or just the classes from the tests source set? Is it configurable somehow, if we want to only scan classes under specific package?
s
Depends where you are invoking the tests from
If using junit5 platform in gradle, gradle will pass the discovered tests through (it looks in the test source set)
If using the intellij plugin, it will use the classname / package name directly
w
(Context: after upgrading Android Gradle Plugin something has changed with how it handles Databinding classes. As a result, Kotest discover engine seems to trigger static initialisation of some of those Databinding classes, and that fails because static initialisers there use Android Framework dependencies 😕)
If using junit5 platform in gradle, gradle will pass the discovered tests through
You mean tests or test classes? And Kotest will still filter those and only pick classes that extend from a Spec?
s
It depends on the version/setup but gradle is supposed to pass in a discovery request, which includes things like classpath / package name, and then kotest scans those packages
w
And that’s
discover
method in
KotestJunitPlatformTestEngine
, right?
s
which version of kotest ?
w
Oh, I see sources of runner-jvm-4.2.5 🤔
s
There's
kotest-framework-discovery
module which contains the code
the discovery method in
Copy code
KotestJunitPlatformTestEngine
uses that depending on what gradle / intellij / whatever passes in
If it's asking for a specific class, there's no need to run discovery
That is triggering the inits
w
But if we run tests from entire module,
./gradlew foo:test
the request will scan the classpath, right?
s
yes
It's not supposed to actually load the classes though, but perhaps that's a bug and it is
w
yep, I discovered the
doDiscover
earlier 🙂 Just wanted to confirm that’s a good direction
It actually might be a bug, because what we’re observing is
Copy code
public class DataBinderMapperImpl extends DataBinderMapper {
  private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(0);

  private static class InnerBrLookup {
    static final SparseArray<String> sKeys = new SparseArray<String>(22);
  }
}
The offending line is the
InnerBrLookup
, not the outer one. I’m not sure if it’s because of the order of how the classes are loaded or it’s something about the class being an inner one
https://github.com/mateuszkwiecinski/databinding_doesnt_work/tree/fixme_pls here’s an almost repro: it doesn’t depend on Kotest (was created before we figured out the exact problem) but if you replace vintage runner with Kotest, in library module, it’ll start failing on tests discovery
s
Let me add a test case
🙏 1
Copy code
class Foo {
   companion object {
      init {
         error("boom")
      }
   }
}
That should be the same right
That causes the exception to throw when running the tests. Is your DataBinderMapperImpl in main or test.
w
I believe it’s in both, although I don’t fully understand what AGP does with the classpath there
But I’m gonna say it’s in tests because part of AGP 4.1 migration is running
kaptTest
so the classes would have to be available in tests now
s
So if I have this class, Foo, that's never referenced from any test, it shouldn't be loaded by the classloader (in theory) and the init block should never fire
At least if my understanding of the JVM is correct
w
Yes, that’s my understanding as well
And my understanding after some digging (but I’m not 100% sure) is that Kotest does load these classes somehow anyway, the nested one at least anyway
Okay that thing you pasted isn’t the same, as there’s no
static { }
block in decompiled bytecode in the nested class, and the error is in the companion constructor. Not sure if it should technically make a difference, just pointing out it’s different
s
Ok fair enough, I can try putting the java class in
or use a @JvmStatic on a field that throws
w
Copy code
class Foo {
    class Bar {
        companion object {

            private val x: Nothing = error("boom")
        }
    }
}
This is more similar, as there’s now static block that throws, in the inner class
s
ok. Something odd is going on, because it's being invoked before kotest's discovery
🎉 1
w
Not happy because it’s happening, happy because it’s a bug and we have a repro, right?
s
yes
once you have the repro, the rest is easy 🙂
ok so here's the issue
Gradle is sending over a list of classnames, and kotest is then doing Class.forName on them to find out if they're specs or not
and of course, Class.forName will trigger the static inits
so how do we find out if a class is a Spec without using Class.forName
w
There’s also
forName(String name, boolean initialize, ClassLoader loader)
, which as I understand shouldn’t trigger class initialization if
initialize = false
. However I don’t know if it changes something in the resulting class
s
wow I've been doing reflection stuff since 1999 and never seen that method before
😄 1
and it says it was added in 1.2
Ok that method seems to help
I can use that first to filter down to specs and then run the inits after
w
Is it required to manually init classes?
s
To be safe I'm doing this
Copy code
// first filter down to spec instances only, then load the full class
val loadedClasses = request
   .selectors
   .filterIsInstance<DiscoverySelector.ClassDiscoverySelector>()
   .map { Class.forName(it.className, false, this::class.java.classLoader) }
   .filter(isSpecSubclass)
   .map { Class.forName(it.name).kotlin }
   .filterIsInstance<KClass<out Spec>>()
w
I don’t understand what’s happening exactly, I just assumed JVM would initialize the classes whenever needed anyway
s
I'm doing the 2nd class.forname because I don't know if the first time, by passing in false, it's going to mess with the class structure. Just to be safe, since we know it's a spec, I load it again the normal way.
w
Right, they’d have to be loaded at some point anyway 👍
PR looks good, just seems to have unintended
enabled()
change 🙂
I can check if a snapshot works as expected
s
oops reverted that enabled change, thanks
yeah check the snapshot once I merge. And if good, I'll release 4.2.7 and 4.3.1 with the fix.
I'm surprised no one has ran into this issue before
w
I still don’t understand what Android Gradle Plugin was doing that this issue wasn’t triggered before 😕 Also I wonder if there’ll be any performance improvement from not initializing all the classes
s
Yeah it shouldn't make it slower at least 🙂
👀 1
w
@sam confirmed the snapshot works and fixes the issue in all our tests 🙂
s
awesome 🙂
Will release 4.3.1 and 4.2.7 soon
🙏 1
w
Thank you very much. I recall the responsiveness to issues was why I chose Kotest over Spek back ~2 years ago and I’m glad to see that was the right choice 🙂
s
Thanks, that's very nice to hear 🙂
We wouldn't be able to fix issues as quickly if it wasn't for users like you who help find and test
w
😂 1
p
nice find & fix guys
👍🏻 1
c
maybe you could try to not load it a second time. if tests run multithreaded it could improve performance because its good to keep the single threaded init part as short as possible.
👍🏻 1