Opinions on plugin design

I am developing another plugin and was hoping to get some opinions/thoughts/advice on the overall design, especially given all of the development on Gradle since I last worked on a plugin.

The goal of the plugin is simple - to be able to define a series of “database profiles” and enable one for a build… the idea being to dynamically run a project’s tests against a selected database profile. Ultimately this just means the ability to augment test resources (either merge Properties or filtered copies).

The initial design is to expose an extension object that allows the project to control the profile used. This caused a problem in that I have to then wait until after project evaluation to be able to resolve the profile to use, since it is configured in the project via extension. I toyed around with the new Provider/Property contracts to work around this, but as the database-profile info is often needed to configure a task, its a chicken-egg and the Provider/Property would still be resolved too early.

The main concern is that “augmenting test resources” means any number (and sometimes more than one) thing to be done. E.g. in Hibernate build I have to (1) augment build/resources/test/hibernate.properties with any profile-defined properties and (2) copy-with-filter a number of other files. I saw 2 ways to accomplish that:

  1. Allow the user/project to define the tasks themselves, but this runs into the chicken-egg thing - how do they reference something during that task’s configuration when that something is only known after evaluation?
  2. Expose behavior on the extension to create actions for the “augment” and “copy-with-filter” behaviors. E.g.:
databaseProfile {
    // ^^ my extension object
    use = 'h2'
    augment file( "${buildDir}/resources/test/hibernate.properties" )

    filterCopy {
        from file('src/test/bundles')
        into ...

How “kosher” is this second approach? And if the answer is “not at all”, what is the better solution?

Part of the problem is that I want to make sure that the profile-to-use is only resolved once for the project - that is an expensive operation involving file-system searches

P.S. Hans had mentioned to me earlier a feature where I could dynamically interpret non-explicit task names and route the execution to something else. That vague enough? :stuck_out_tongue: The idea IIRC was to allow the user to execute a task named testOnH2, testOnPgsql, etc and be able to react as if they had called test -Pdb=h2. I cannot remember the name of that feature so cannot really make an educated google search. Any pointers?

You can use task rules to create the tasks on the fly based on naming convention. There you can encode (based on the same naming convention) which property file gets picked, etc.

Rules - that was it… Thanks Dimitar!

Any thoughts on the rest?

Just when I finally have a use case for “Rule based model configuration” it apparently goes straight from incubating to deprecated. Ugh.

This is really my main concern with this plugin - timing. The plugin has to perform a number of operations based on the profiles available which it does not know until the user has configured its extension object. So almost its entire work (aside from creating the extension) is delayed until afterEvaluation. But isn’t this exactly why “Rule based model configuration” was developed in the first place? To resolve situations like this? It seems like I am back to the old adage of “write statements in the build script in specific order” to address this.

Specifically, in order, the plugin needs to:

  1. Locate all available profiles (needs to wait for user config of extension)
  2. Resolve the selected profile (again needs extension config)
  3. Create a task rule for running tests with each available profile (needs extension fully configured)
  4. Perform various “task actions” based on selected profile (needs extension fully configured)

Surely there is some Gradle-y way to handle this beyond sketchy hand-crafting via afterEvaluation?

P.S. If the feature is deprecated, maybe the annotations etc should be updated from @Incubating to @Deprecated?

Rule based model configuration (deprecated) and Task Rules (what you described in your original P.S. and named by Dimitar) are different concepts.

Using task rules, you’d define the rule for a task to handle any profile. The logic in the rule creates the actual task on demand, so you don’t need to worry about available profiles ahead of time. Once the task is executing, the extension has been configured, so you can try to resolve the selected profile or throw an error if it’s not valid. Then run your task actions with what was resolved.

This is the same mechanism that allows clean<TaskName> to delete the outputs of taskName without declaring a Delete task for every task. It uses the taskName in clean<TaskName> to lookup the task, and configure the Delete task to delete those output files. Your rule would be for testOn<Profile>, but you’d lookup the profile in the extension instead of the task container.

You misunderstand me. Yes there are 2 different concepts and 2 different things being discussed. Task rules, which Dimitar pointed me to, will be useful.

However in my last reply I am clearly talking about Rule based model configuration. Task rules have no annotations that would need to be either @Incubating nor @Deprecated. This part is about “timing”.

I just ended up removing the ability to configure this via an extension and required the use of System/Project properties. Sad that I have to go that route, but sans model { ... } its just too fugly to implement. Shame.