Skip to content

Conversation

@crogoz
Copy link
Contributor

@crogoz crogoz commented Oct 27, 2025

Before this PR

Groovy Test spec that runs with Configuration Cache enabled.

After this PR

Ability to run GradlePluginTests with "--configuration-cache" enabled when the extension properrty gradleTestUtils.isConfigurationCacheEnabled is set.

Based on the value of the configuration cache, we will instantiate a DefaultGradleInvoker or a ConfigurationCacheInvoker.

The ConfigurationCacheInvoker will do:

  1. run the tasks with --configuration-cache added to the list of arguments. It will throw an UnexpectedConfigurationCacheFailure exception if the configuration cache was not stored/failed.
  2. run the tasks above including --configuration-cache & --dry-run to make sure the configuration cache was reused. It will throw an UnexpectedConfigurationCacheFailure if not.

Added a new exception type UnexpectedInvocationResult with the corresponding UnexpectedInvocationFailure & UnexpectedInvocationSuccess that are wrappers on top of gradle testkit's exceptions UnexpectedBuildFailure & UnexpectedBuildSuccess.

Added a new DisabledConfigurationCache which will disable per test class/method the configuration-cache. This will be useful later when we are migrating the GradlePluginTests to use by default the configuration-cache and we will need to disable the tests that are incompatible with cc.

==COMMIT_MSG==
Ability to run GradlePluginTests with configuration-cache.
==COMMIT_MSG==

Possible downsides?

@changelog-app
Copy link

changelog-app bot commented Oct 27, 2025

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

Add support for running GradlePluginTests with configuration-cache

Check the box to generate changelog(s)

  • Generate changelog entry

.withEnvironment(gradleRunner.getEnvironment())
.withArguments(ImmutableList.<String>builder()
.addAll(gradleRunner.getArguments())
.add("--dry-run")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for config-cache to be reused, we need to run the same tasks. In order to avoid running the same tasks twice, we can pass a --dry-run.
Tested that config cache is triggered by a dry-run:

Reusing configuration cache.
:compileJava SKIPPED
:help SKIPPED

BUILD SUCCESSFUL in 62ms
Configuration cache entry reused.

assertConfigCacheReused(configurationCacheResult.output());
}

return result;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always returning the result of the first invocation.

@crogoz crogoz marked this pull request as ready for review October 27, 2025 16:26
@crogoz crogoz requested a review from CRogers October 27, 2025 16:26
public final class GradleInvocation {
private final GradleRunner gradleRunner;
private final GradleVersion gradleVersion;
private final boolean configurationCacheEnabled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than taking a boolean and doing if statements, can we instead try and change GradleInvoker and GradleInvocation into interfaces, and have a BasicGradleInvoker and a ConfigurationCacheGradleInvoker (likely the same for GradleInvocation too). The configuration cache versions should just be able to call the basic versions as many times as they need - we might be able to avoid copying the code for creating the GradleRunner again on line 50 too.

final class ConfigurationCacheTest {

@Test
void testConfigurationCache(GradleInvoker invoker, RootProject rootProject) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test methods should not start with test, it's superfluous. Additionally, test names should be a snake_case_english_sentence.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think we should make an errorprone check for this, to ensure that people do this for our Gradle plugins tests.


// Run a second time to verify the cache is reused, we don't need to run the tasks --dry-run is enough to
// check if the configuration cache was used.
GradleRunner configurationCacheRunner = GradleRunner.create()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen cases where the second run doesn't reuse the cache but the subsequent do - normally related to file generation. I'm not sure if that behaviour is ever valid so the solution might just be that we want to fail those cases but could also be nice to have some kind of ratchet - although in those edge cases we could just get users to write there CC tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure that was the case here https://pl.ntr/2xX - but its possible I also just didn't fix it properly!

record MyClass() {}
""");

InvocationResult result = invoker.withArgs("compileJava", "help").buildsSuccessfully();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is currently testing?

In terms of tests, I think we could have at least a few tests:

  • In the build.gradle, print whether the configuration cache is enabled and check it is disabled when @WithConfigurationCache is not present, and is enabled when present. You can use the start parameter / configuration cache build feature. This actually raises a good questions - we might not want to run with configuration on Gradle 7, as the implementation and error messages are bad.
  • Fails when there is a basic configuration cache error eg executing a process at configuration time
  • Fails when there is an issue storing the cache entry
  • Fails when there is an issue loading the cache entry
  • Succeeds when there are no configuration cache issues

Comment on lines 95 to 96
"Running @WithConfigurationCache: Expected configuration cache entry to be reused, but it"
+ " wasn't. Output: %s",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, especially for the second invocation, we need a really good error message that is very explicit in what happened. We need to tell the dev that first invocation worked and stored the cache entry, but then we ran again with --dry-run to verify that loading the cache still worked and this failed, and how they should diagnose it (look at the report? build scan? can we parse these from the output and put it in the error message?)

File configurationCacheDir = new File(gradleRunner.getProjectDir(), ".gradle/configuration-cache");
assertThat(configurationCacheDir.exists())
.as(String.format(
"Running @WithConfigurationCache: Expected the configuration-cache %s directory to exits.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/exits/exist

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(ConfigurationCacheExtension.class)
public @interface WithConfigurationCache {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of actually rolling this out, not sure how I feel about opting in (like this with @WithConfigurationCache) vs a global opt in at the Gradle extesion level and opting out with @ConfigurationCacheDisabled. Or even further - default having configuration cache on without any Gradle configuration and relying on a disable annotation.

My worry with the opting in is people will just forget to add it to new tests and we want to ensure all new code/test code is configuration cache compatible.

But in order for upgrades to not get blocked with opt out, we need to work out a way to feed back test failures to the excavator which will enable it to add @ConfigurationCacheDisabled onto the relevant tests/test classes. Opt-in, it's a little easier as you can make a parameterised excavator that tries to opt in each test class, then the excavator will just not merge if it doesn't work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was to add this flag to all tests that are extending ConfigurationCacheSpec example here. This way we can do the migration of the nebula tests -> new java framework 1-1.

Then I think it would be pretty easy to add an extension similar to how we set the Gradle versions and make it by default using the CC and add a workflow to add the @ConfigurationCacheDisabled. But I am thinking that would be a separate migration from the nebula test -> new Java framework. wdyt ?

@crogoz crogoz force-pushed the cr/tests-with-config-cache branch from 5233348 to 60d02f2 Compare October 31, 2025 10:37
@crogoz crogoz changed the title @WithConfigurationCache Annotation for GradlePluginTests Add support for running GradlePluginTests with configuration-cache Oct 31, 2025
@changelog-app
Copy link

changelog-app bot commented Oct 31, 2025

Successfully generated changelog entry!

Need to regenerate?

Simply interact with the changelog bot comment again to regenerate these entries.


📋Changelog Preview

💡 Improvements

  • Add support for running GradlePluginTests with configuration-cache (#245)

// `withDebug(true)` will run the Gradle tooling inside the same JVM as the test, meaning
// debugging works, whereas `withDebug(false)` will run Gradle in a new daemon. There can be
// differences between these two modes!
return ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FLUP: cc is not compatible with testkit's debugging option see issue: gradle/issues/25846

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, we should probably just disable configuration cache when debugging and paste a warning log line that we've done so in each test. This is what we do in suppressible-error-prone, would be great to get it in the framework directly.

assertThat(gradle.withArgs().buildsSuccessfully().output()).contains("project name: root");
InvocationResult invocation = gradle.withArgs().buildsSuccessfully();
assertThat(invocation.output()).contains("hello from :");
assertThat(invocation.output()).contains("project name: root");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've enabled cc here. Previously we were running the tasks twice, which means that cc will not run the second time - hence the second run will not contain the expected output lines. Same below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting! I have a feeling this more than anything may break existing tests that use printlns at configuration time a lot (though I guess they should be doing it within task actions if they were good).

I wonder if we wanted tests to be more or less configuration cache agnostic whether we delete the configuration cache dir after checking it can be reloaded with --dry-run. I'm not sure if it's a good idea, but it would still check configuration cache but run the configuration every time. Feels a little dodgy though, maybe we just keep it as an option in case converting existing tests is hard?


tasks.named('test', Test) {
// by default the tests should run with configuration cache enabled.
systemProperty 'com.palantir.gradle.testing.configuration_cache_enabled', 'true'
Copy link
Contributor Author

@crogoz crogoz Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we publish this version, I'll add the isConfigurationCacheEnabled = true in the gradleTestutils extension

import com.palantir.gradle.testing.RestrictedCreation;
import java.nio.file.Path;

public record ConfigurationCacheInvoker(Path projectDir, GradleInvoker gradleInvoker) implements GradleInvoker {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this a regular class so we can hide .gradleInvoker() etc

Comment on lines +29 to +37
@Override
public void beforeAll(ExtensionContext context) {
setConfigurationCache(context);
}

@Override
public void beforeEach(ExtensionContext context) {
setConfigurationCache(context);
}
Copy link
Contributor

@CRogers CRogers Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's safer to just lazily compute whether configuration cache is enabled or not in the store rather than setting them here - can't forget to set it in all the relevant locations then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that's a bit strange, as you need to look at the enclosing elements to find configuration cache, and how would we handle a @ConfigurationCacheDisabled arg on a test when there's a @WithConfigurationCache on the test class...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would we handle a @ConfigurationCacheDisabled arg on a test when there's a @WithConfigurationCache on the test class...

I removed this class in the latest commits. I only kept the @DisableConfigurationCache

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds configuration cache support to GradlePluginTests, enabling tests to verify that Gradle plugins are compatible with configuration cache functionality. The implementation provides both the ability to enable configuration cache by default and to selectively disable it for incompatible tests.

  • Introduces configuration cache support with automatic verification of cache storage and reuse
  • Adds @DisabledConfigurationCache annotation for excluding incompatible tests
  • Refactors the invoker architecture to support both standard and configuration cache execution modes

Reviewed Changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ConfigurationCacheHelper.java Defines system property constant for configuration cache enablement
PluginTestingPlugin.java Adds system property configuration for tests to use configuration cache setting
PluginTestingExtension.java Adds configurationCacheEnabled property with default false value
Multiple test files Comprehensive test coverage for configuration cache functionality and edge cases
GradleInvoker.java Converts from class to interface with factory method for invoker selection
DefaultGradleInvoker.java Implements standard Gradle invocation without configuration cache
ConfigurationCacheInvoker.java Implements configuration cache-aware Gradle invocation with verification
Exception classes New exception hierarchy for handling configuration cache and invocation failures
JUnit integration files Extensions and stores for managing configuration cache state in test context

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

crogoz and others added 7 commits October 31, 2025 17:50
@Override
public InvocationResult buildsWithFailure() {
InvocationResult result = initialGradleInvocation.buildsWithFailure();
assertConfigCacheStored(result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if it fails at configuration time, before executing any tasks? I think this is allowed by buildsWithFailure but the configuration cache entry won't be stored. Should have a test for it at least. Probably want to only check this if a task was executed/it was a task that failed.

public InvocationResult buildsWithFailure() {
InvocationResult result = initialGradleInvocation.buildsWithFailure();
assertConfigCacheStored(result);
return result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we actually want to check we can load configuration cache here if it was a task that failed. If the configuration was fine but a task failed, it should be fine to run with --dry-run (I think?)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth writing a test for.

@Override
public GradleInvocation withArgs(String... args) {
// not reusing configuration-cache among multiple gradle invocations in a test.
cleanupConfigurationCache();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do this here or after a gradle invocation? I guess this might be a bit safer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants