Using non-existent directory (DirectoryProperty) used as task input

I am writing a task and have run into an issue I am unsure how to handle.

class EnhancementTask extends DefaultTask {
    ...
    private final DirectoryProperty javaCompileOutputDirectory;

    public EnhancementTask(...) {
        ...
        javaCompileOutputDirectory = mainSourceSet.getJava().getDestinationDirectory();
    }

    @Internal
    @InputDirectory
    @Incremental
    public DirectoryProperty getJavaCompileDirectory() {
        return javaCompileOutputDirectory;
    }

When running tests with TestKit I get errors like:

A problem was found with the configuration of task ':hibernateEnhance' (type 'EnhancementTask').
> Directory '.../build/classes/java/main' specified for property 'javaCompileDirectory' does not exist.

Now granted this is (so far) just TestKit - maybe it works in “real use”, I am not sure yet. But it really gets into general problems I have with TestKit. Here, for example, I have no Java sources - the trouble being that I have never been able to use TestKit in any way with actual sources. Since there are no Java sources, there is no Java classes directory.

So granted, this might be limited to just the TestKit environment. But I’d still like to be able to handle it in a generalized way to deal with various real-use sce

TestKit or “real use”, you should receive this error if the @InputDirectory does not exist. It’s a required input for the task. If you don’t need it to perform the work of the task, but it is just nice to have, it should be @Optional. If the task requires the input and shouldn’t run if it’s not there, you want to @SkipWhenEmpty.

Hi Justin, thanks for the reply.

Feel free to say the rest of this reply should be a separate topic…

I think the general pattern to the trouble I have with TestKit is that I try to deal with the “test projects” as test-resources (I keep them in a separate “source dir”), but they get processed into the output dir for processTestResources. I surmise that somehow messes up the up-to-date checking between the “real project” and the “test project”.

Do y’all have a better pattern for dealing with test projects that need to include source, etc? Specifically the difficulty I was trying to solve was to be able to locate the test project dirs during runtime.

In the interim (or if there is not a better pattern) I started work on a plugin to manage the test projects used with TestKit.

I’m not really a y’all here as this isn’t anything official. This is just what I did personally the first time I needed to do it, and have stuck with it. My structure generally looks like this for a plugin:

plugin-project
    \- src
        |- main
            |- java
            \- resources
        |- test (unit tests for my encapsulated domain logic - no Gradle API)
            |- java
            \- resources
        |- integTest (integration tests that use ProjectBuilder, but no TestKit)
            |- java
            \- resources
        \- functTest (functional tests that run real builds using TestKit)
            |- java
            \- resources (my test project code is here)

In my functional test source set, I do have a helper class that does things like copy those project files from the classpath to a specific folder on disk. That specific folder is a temporary folder created at test runtime for each and every test. I generally start with a base that is used for pretty much every functional tests and then have a few one off files that overlay on top, so copies of the same files end up being used in many tests, but starting from a completely clean state.

I can’t tell, based on what you said, if you’re maybe trying to run those builds “in place”. I definitely don’t do that, which may be a difference.

Thanks again James.

I took a slightly different, but very similar, approach. I define a structure like:

\- src
  | main ...
  ..
  \- test
    |- java
    |- resources
    \- testkit

The plugin manages things like copying the stuff from src/test/testkit into a defined output dir and adding that dir as a test classpath element (I can then locate the directory via a resource lookup for a file I generate into the output dir).

I do have a helper class that does things like copy those project files from the classpath to a specific folder on disk

Because I don’t let Gradle’s processTestResources task handle the testkit projects it avoids problems with “dirtying” up-to-date checking for that task. Which I assume is the same reason you copy from the classpath to somewhere else. Though correct me if I am wrong.

The plugin has one other feature specific to JUnit 5 that I am also not sure how to deal with. It allows the tests to leverage JUnit’s parameter injection for access to information about the testkit projects. E.g.

class TestKitProjectScope {
	public File getProjectBaseDirectory() { ... }
	public GradleRunner createGradleRunner(String... args) { ... }
}

class Tests {
	...

	@Test
	@TestKitProject( "simple" )
	public void testProjectScope(TestKitProjectScope scope) {
		// `scope` defines
	}
}

This requires the test to have compile-time access to the plugin (the plugin contains the annotations and “scope” contracts). But I have not been able to figure out how to do that part yet. Any thoughts on handling that part?

The end result is the same, but the motivation for doing so in the first place is completely opposite. It really has nothing to do with Gradle, TestKit, or the build itself. For the purpose of the discussion, let’s remove those entirely from the picture to start with.

Instead, say I’m writing tests for some file copying utilities. The content of the files is irrelevant. I can generate some random characters each time or write out a constant line each time. I just care that the code copies the file successfully. My starting point for a JUnit test is to declare a @TempDir folder for the test, which is still safe if I get into a situation that my tests could run concurrently. My @BeforeEach setup is going to be writing something arbitrary to a source file. Then I’ll run the copy utility with the source and destination. Finally, I’ll verify that the destination now exists and that it contains the expected content. JUnit will remove the directory for me when the test is done. No matter what I’m trying to do, if the test requires a location on disk, I’m going to handle it this way.

Back to Gradle, there’s really no difference between my file utilities case and my Gradle build using TestKit. However, maybe the contents of my file do matter now. Still, nothing stops from me from writing a file from the test code that writes out plugins { id 'java' } to a build.gradle file in that @TempDir. In fact, a lot of tests written in Groovy do exactly that because it’s so easy to just write an exact multi-line String to a file in Groovy. Absolutely nothing says that my build I want to test with can’t be created entirely programmatically and I might not even have a src/test/resources folder, but no matter what I wouldn’t go write files to the source set from my test.

Hopefully, you now see where I’m going with this. If I decide that I’m going to instead create files to use in my build rather than try to write them from the test itself, that’s a minor implementation detail change. I can copy them from a directory, download from URL, or copy them from the classpath. It doesn’t matter what I do though. That implementation detail is never going to cause me to say that I should now treat that source file location as my build folder. My build folder is still the @TempDir given to me by JUnit for each and every test. I may have 100 tests, but I only need 1 sample file in src/main/java to use for compilation that gets copied to every single project. My files in src/functTest/resources end up being just a handful of template files, but they may be included in 60 different ways or combinations depending on what the test needs, maybe with a single configuration line appended to the end by the test setup itself. So, the motivation here really has nothing to do with not “dirtying” the up-to-date checking for that task. It’s just never a problem if I treat those resources just as resources and then utilize @TempDir for what it’s designed to be used for in the tests.

I’m not sure I really understand the issue. This sounds like something you would implement in a separate project and just depend on as a library in your tests, just like JUnit itself.

1 Like

That is a lot to digest… I’ll have to think about it.

One quick thought…

Yes the end result of the plugin (the main goal anyway) is to “copy files”. But these files are logically related, which is an important distinction, to me at least. In this sense, it is really a Source(Directory)Set. For me, it is completely reasonable to handle these specially - beyond just a source for CopySpec. You do the same, though for different reasons as you outlined. Out of curiosity, does your src/functTest/resources contain only the TestKit projects? Or other resources (log4j.properties, e.g.) as well?

That is definitely an option. I had considered it. The concern is simply KISS. This approach would require me to specify 2 different dependencies (the plugin and then the library) and make sure the versions in the build stay in sync. Especially considering this is a personal plugin (this is just my take on TestKit) and will only ever be used with Gradle + Junit5. Have you really never had a situation where it would be nice to have the plugin contribute something to your “real” classpaths?

FWIW I got it to “work” via hack

configurations.testImplementation.extendsFrom( buildscript.configurations.classpath )

I guess I’ll just add a hack in the plugin to find its artifact in buildscript.configurations.classpath and apply it to testImplementation

Anyway, thanks again for your replies James

After reading through your reply I understand why you do the things you do.

But unless I am missing something, there still seemed to be a a fair amount of scaffolding in order to functionally test a plugin. So I went ahead and finished the plugin I was working on to help with that scaffolding.


In case anyone finds it useful.

Thanks a lot again James. Your replies really got me on the right track :slight_smile: