From f31415aa99db8951906958f8a865c33cbba382b7 Mon Sep 17 00:00:00 2001 From: Eric Haag Date: Sat, 29 Jun 2024 23:51:20 -0500 Subject: [PATCH] Add more tests and DevelocityClient builder --- .../develocity/api/AccessKeyProvider.java | 5 +- .../develocity/api/DevelocityClient.java | 14 +- .../api/HttpClientDevelocityClient.java | 15 +- .../HttpClientDevelocityClientBuilder.java | 34 +++ .../processing/BuildListenerBuilder.java | 20 +- .../develocity/processing/BuildProcessor.java | 23 +- .../processing/BuildProcessorBuilder.java | 2 +- .../processing/BuildProcessorWorker.java | 4 +- .../processing/ProcessListener.java | 4 + .../processing/ProcessListenerBuilder.java | 117 +++++++++ .../processing/BuildProcessorTest.java | 234 ++++++++++++++++++ .../processing/cache/CompositeCacheTest.java | 77 +++--- .../processing/cache/FileSystemCacheTest.java | 24 +- .../processing/cache/InMemoryCacheTest.java | 15 +- .../dev/erichaag/develocity/api/Builds.java | 103 +++++++- .../develocity/api/DevelocityClientStub.java | 61 +++++ .../processing/cache/AbstractCacheTest.java | 154 ++++++++++-- 17 files changed, 802 insertions(+), 104 deletions(-) create mode 100644 src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClientBuilder.java create mode 100644 src/main/java/dev/erichaag/develocity/processing/ProcessListenerBuilder.java create mode 100644 src/test/java/dev/erichaag/develocity/processing/BuildProcessorTest.java create mode 100644 src/testFixtures/java/dev/erichaag/develocity/api/DevelocityClientStub.java diff --git a/src/main/java/dev/erichaag/develocity/api/AccessKeyProvider.java b/src/main/java/dev/erichaag/develocity/api/AccessKeyProvider.java index dcdfa97..dcac32a 100644 --- a/src/main/java/dev/erichaag/develocity/api/AccessKeyProvider.java +++ b/src/main/java/dev/erichaag/develocity/api/AccessKeyProvider.java @@ -25,13 +25,14 @@ public final class AccessKeyProvider { private AccessKeyProvider() { } - public static Optional lookupAccessKey(URI serverUrl) { + public static String lookupAccessKey(URI serverUrl) { return fromEnvVar(accessKey, serverUrl) .or(() -> fromEnvVar(legacyAccessKey, serverUrl)) .or(() -> fromGradleHome("develocity", serverUrl)) .or(() -> fromMavenHome("develocity", serverUrl)) .or(() -> fromGradleHome("enterprise", serverUrl)) - .or(() -> fromMavenHome("gradle-enterprise", serverUrl)); + .or(() -> fromMavenHome("gradle-enterprise", serverUrl)) + .orElseThrow(() -> new RuntimeException("No access key found for server " + serverUrl.getHost())); } private static Optional fromGradleHome(String baseDir, URI serverUrl) { diff --git a/src/main/java/dev/erichaag/develocity/api/DevelocityClient.java b/src/main/java/dev/erichaag/develocity/api/DevelocityClient.java index 4798996..c861008 100644 --- a/src/main/java/dev/erichaag/develocity/api/DevelocityClient.java +++ b/src/main/java/dev/erichaag/develocity/api/DevelocityClient.java @@ -1,13 +1,15 @@ package dev.erichaag.develocity.api; +import java.net.URI; import java.util.List; +import java.util.Optional; import java.util.Set; public interface DevelocityClient { - Build getBuild(String id, Set buildModels); + Optional getBuild(String id, Set buildModels); - default Build getBuild(String id, BuildModel... buildModels) { + default Optional getBuild(String id, BuildModel... buildModels) { return getBuild(id, Set.of(buildModels)); } @@ -17,4 +19,12 @@ default List getBuilds(String query, Integer maxBuilds, String return getBuilds(query, maxBuilds, fromBuild, Set.of(buildModels)); } + static HttpClientDevelocityClientBuilder forServer(URI serverUrl) { + return new HttpClientDevelocityClientBuilder(serverUrl); + } + + static HttpClientDevelocityClientBuilder forServer(String serverUrl) { + return forServer(URI.create(serverUrl)); + } + } diff --git a/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClient.java b/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClient.java index f63deff..3419b57 100644 --- a/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClient.java +++ b/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClient.java @@ -15,11 +15,13 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import static java.net.http.HttpResponse.BodyHandlers.ofByteArray; +import static java.util.Optional.empty; public final class HttpClientDevelocityClient implements DevelocityClient { @@ -30,17 +32,20 @@ public final class HttpClientDevelocityClient implements DevelocityClient { private static final int maxRetries = 5; - public HttpClientDevelocityClient(URI serverUrl) { + public HttpClientDevelocityClient(URI serverUrl, String accessKey, HttpClient httpClient) { this.serverUrl = serverUrl; - this.accessKey = AccessKeyProvider.lookupAccessKey(serverUrl).orElse(null); - this.httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); + this.accessKey = accessKey; + this.httpClient = httpClient; this.objectMapper = new JsonMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } @Override - public Build getBuild(String id, Set buildModels) { + public Optional getBuild(String id, Set buildModels) { final var response = sendRequest("/api/builds/" + id, null, false, null, null, buildModels); - return Build.from(handleResponse(response, new TypeReference<>() {})); + if (response.statusCode() == 404) { + return empty(); + } + return Optional.of(Build.from(handleResponse(response, new TypeReference<>() {}))); } @Override diff --git a/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClientBuilder.java b/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClientBuilder.java new file mode 100644 index 0000000..45c643a --- /dev/null +++ b/src/main/java/dev/erichaag/develocity/api/HttpClientDevelocityClientBuilder.java @@ -0,0 +1,34 @@ +package dev.erichaag.develocity.api; + +import java.net.URI; +import java.net.http.HttpClient; + +import static dev.erichaag.develocity.api.AccessKeyProvider.lookupAccessKey; + +public final class HttpClientDevelocityClientBuilder { + + private final URI serverUrl; + private final HttpClient.Builder httpClientBuilder = HttpClient.newBuilder(); + + private boolean useAnonymousAccess = false; + + HttpClientDevelocityClientBuilder(URI serverUrl) { + this.serverUrl = serverUrl; + } + + public HttpClientDevelocityClient build() { + final var accessKey = useAnonymousAccess ? null : lookupAccessKey(serverUrl); + return new HttpClientDevelocityClient(serverUrl, accessKey, httpClientBuilder.build()); + } + + public HttpClientDevelocityClientBuilder withAnonymousAccess() { + this.useAnonymousAccess = true; + return this; + } + + public HttpClientDevelocityClientBuilder followingRedirects() { + this.httpClientBuilder.followRedirects(HttpClient.Redirect.ALWAYS); + return this; + } + +} diff --git a/src/main/java/dev/erichaag/develocity/processing/BuildListenerBuilder.java b/src/main/java/dev/erichaag/develocity/processing/BuildListenerBuilder.java index 144a63b..9e23ecf 100644 --- a/src/main/java/dev/erichaag/develocity/processing/BuildListenerBuilder.java +++ b/src/main/java/dev/erichaag/develocity/processing/BuildListenerBuilder.java @@ -64,51 +64,51 @@ public BuildListenerBuilder requiredBuildModels(BuildModel... buildModels) { return this; } - public BuildListenerBuilder onBuild(Consumer consumer) { + public BuildListenerBuilder onBuild(Consumer onBuild) { listeners.add(new BuildListener() { @Override public void onBuild(Build build) { - consumer.accept(build); + onBuild.accept(build); } }); return this; } - public BuildListenerBuilder onGradleBuild(Consumer consumer) { + public BuildListenerBuilder onGradleBuild(Consumer onGradleBuild) { listeners.add(new BuildListener() { @Override public void onGradleBuild(GradleBuild build) { - consumer.accept(build); + onGradleBuild.accept(build); } }); return this; } - public BuildListenerBuilder onMavenBuild(Consumer consumer) { + public BuildListenerBuilder onMavenBuild(Consumer onMavenBuild) { listeners.add(new BuildListener() { @Override public void onMavenBuild(MavenBuild build) { - consumer.accept(build); + onMavenBuild.accept(build); } }); return this; } - public BuildListenerBuilder onBazelBuild(Consumer consumer) { + public BuildListenerBuilder onBazelBuild(Consumer onBazelBuild) { listeners.add(new BuildListener() { @Override public void onBazelBuild(BazelBuild build) { - consumer.accept(build); + onBazelBuild.accept(build); } }); return this; } - public BuildListenerBuilder onSbtBuild(Consumer consumer) { + public BuildListenerBuilder onSbtBuild(Consumer onSbtBuild) { listeners.add(new BuildListener() { @Override public void onSbtBuild(SbtBuild build) { - consumer.accept(build); + onSbtBuild.accept(build); } }); return this; diff --git a/src/main/java/dev/erichaag/develocity/processing/BuildProcessor.java b/src/main/java/dev/erichaag/develocity/processing/BuildProcessor.java index e2aaf01..f190fd3 100644 --- a/src/main/java/dev/erichaag/develocity/processing/BuildProcessor.java +++ b/src/main/java/dev/erichaag/develocity/processing/BuildProcessor.java @@ -1,13 +1,18 @@ package dev.erichaag.develocity.processing; +import dev.erichaag.develocity.api.Build; import dev.erichaag.develocity.api.BuildModel; import dev.erichaag.develocity.api.DevelocityClient; import dev.erichaag.develocity.processing.cache.ProcessorCache; import java.time.Instant; import java.util.List; +import java.util.Optional; import java.util.Set; +import static java.util.Objects.requireNonNullElse; +import static java.util.Objects.requireNonNullElseGet; +import static java.util.Optional.empty; import static java.util.stream.Collectors.toUnmodifiableSet; public final class BuildProcessor { @@ -28,8 +33,8 @@ public final class BuildProcessor { List buildListeners, List processListeners) { this.develocity = develocity; - this.processorCache = processorCache; - this.maxBuildsPerRequest = maxBuildsPerRequest == null ? defaultMaxBuildsPerRequest : maxBuildsPerRequest; + this.processorCache = requireNonNullElseGet(processorCache, NoCache::new); + this.maxBuildsPerRequest = requireNonNullElse(maxBuildsPerRequest, defaultMaxBuildsPerRequest); this.buildListeners = buildListeners; this.processListeners = processListeners; this.requiredBuildModels = buildListeners.stream() @@ -57,4 +62,18 @@ public void process(Instant since, String query) { requiredBuildModels).process(); } + private static final class NoCache implements ProcessorCache { + + @Override + public Optional load(String id, Set requiredBuildModels) { + return empty(); + } + + @Override + public void save(Build build) { + + } + + } + } diff --git a/src/main/java/dev/erichaag/develocity/processing/BuildProcessorBuilder.java b/src/main/java/dev/erichaag/develocity/processing/BuildProcessorBuilder.java index 00ed942..4efbc7e 100644 --- a/src/main/java/dev/erichaag/develocity/processing/BuildProcessorBuilder.java +++ b/src/main/java/dev/erichaag/develocity/processing/BuildProcessorBuilder.java @@ -15,7 +15,7 @@ public final class BuildProcessorBuilder { private ProcessorCache processorCache; private int maxBuildsPerRequest; - public BuildProcessorBuilder(DevelocityClient develocity) { + BuildProcessorBuilder(DevelocityClient develocity) { this.develocity = develocity; } diff --git a/src/main/java/dev/erichaag/develocity/processing/BuildProcessorWorker.java b/src/main/java/dev/erichaag/develocity/processing/BuildProcessorWorker.java index 505f0d9..88bcfba 100644 --- a/src/main/java/dev/erichaag/develocity/processing/BuildProcessorWorker.java +++ b/src/main/java/dev/erichaag/develocity/processing/BuildProcessorWorker.java @@ -103,7 +103,9 @@ private void processCachedBuild(Build cachedBuild) { notifyListenersCachedBuild(cachedBuild); return; } - final var build = develocity.getBuild(cachedBuild.getId(), requiredBuildModels); + // The build has to exist given that it was previously discovered. + //noinspection OptionalGetWithoutIsPresent + final var build = develocity.getBuild(cachedBuild.getId(), requiredBuildModels).get(); processorCache.save(build); notifyListenersFetchedBuild(build); } diff --git a/src/main/java/dev/erichaag/develocity/processing/ProcessListener.java b/src/main/java/dev/erichaag/develocity/processing/ProcessListener.java index d28024f..5ed74cf 100644 --- a/src/main/java/dev/erichaag/develocity/processing/ProcessListener.java +++ b/src/main/java/dev/erichaag/develocity/processing/ProcessListener.java @@ -9,6 +9,10 @@ public interface ProcessListener { + static ProcessListenerBuilder builder() { + return new ProcessListenerBuilder(); + } + default void onCachedBuild(CachedBuildEvent event) { } diff --git a/src/main/java/dev/erichaag/develocity/processing/ProcessListenerBuilder.java b/src/main/java/dev/erichaag/develocity/processing/ProcessListenerBuilder.java new file mode 100644 index 0000000..a67a6aa --- /dev/null +++ b/src/main/java/dev/erichaag/develocity/processing/ProcessListenerBuilder.java @@ -0,0 +1,117 @@ +package dev.erichaag.develocity.processing; + +import dev.erichaag.develocity.processing.event.CachedBuildEvent; +import dev.erichaag.develocity.processing.event.DiscoveryFinishedEvent; +import dev.erichaag.develocity.processing.event.DiscoveryStartedEvent; +import dev.erichaag.develocity.processing.event.FetchedBuildEvent; +import dev.erichaag.develocity.processing.event.ProcessingFinishedEvent; +import dev.erichaag.develocity.processing.event.ProcessingStartedEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public final class ProcessListenerBuilder { + + private final List listeners = new ArrayList<>(); + + ProcessListenerBuilder() { + } + + public ProcessListener build() { + return new ProcessListener() { + + @Override + public void onCachedBuild(CachedBuildEvent event) { + listeners.forEach(it -> it.onCachedBuild(event)); + } + + @Override + public void onFetchedBuild(FetchedBuildEvent event) { + listeners.forEach(it -> it.onFetchedBuild(event)); + } + + @Override + public void onDiscoveryStarted(DiscoveryStartedEvent event) { + listeners.forEach(it -> it.onDiscoveryStarted(event)); + } + + @Override + public void onDiscoveryFinished(DiscoveryFinishedEvent event) { + listeners.forEach(it -> it.onDiscoveryFinished(event)); + } + + @Override + public void onProcessingStarted(ProcessingStartedEvent event) { + listeners.forEach(it -> it.onProcessingStarted(event)); + } + + @Override + public void onProcessingFinished(ProcessingFinishedEvent event) { + listeners.forEach(it -> it.onProcessingFinished(event)); + } + + }; + } + + public ProcessListenerBuilder onCachedBuild(Consumer onCachedBuild) { + listeners.add(new ProcessListener() { + @Override + public void onCachedBuild(CachedBuildEvent event) { + onCachedBuild.accept(event); + } + }); + return this; + } + + public ProcessListenerBuilder onFetchedBuild(Consumer onFetchedBuild) { + listeners.add(new ProcessListener() { + @Override + public void onFetchedBuild(FetchedBuildEvent event) { + onFetchedBuild.accept(event); + } + }); + return this; + } + + public ProcessListenerBuilder onDiscoveryStarted(Consumer onDiscoveryStarted) { + listeners.add(new ProcessListener() { + @Override + public void onDiscoveryStarted(DiscoveryStartedEvent event) { + onDiscoveryStarted.accept(event); + } + }); + return this; + } + + public ProcessListenerBuilder onDiscoveryFinished(Consumer onDiscoveryFinished) { + listeners.add(new ProcessListener() { + @Override + public void onDiscoveryFinished(DiscoveryFinishedEvent event) { + onDiscoveryFinished.accept(event); + } + }); + return this; + } + + public ProcessListenerBuilder onProcessingStarted(Consumer onProcessingStarted) { + listeners.add(new ProcessListener() { + @Override + public void onProcessingStarted(ProcessingStartedEvent event) { + onProcessingStarted.accept(event); + } + }); + return this; + } + + public ProcessListenerBuilder onProcessingFinished(Consumer onProcessingFinished) { + listeners.add(new ProcessListener() { + @Override + public void onProcessingFinished(ProcessingFinishedEvent event) { + onProcessingFinished.accept(event); + } + }); + return this; + } + +} diff --git a/src/test/java/dev/erichaag/develocity/processing/BuildProcessorTest.java b/src/test/java/dev/erichaag/develocity/processing/BuildProcessorTest.java new file mode 100644 index 0000000..e12382d --- /dev/null +++ b/src/test/java/dev/erichaag/develocity/processing/BuildProcessorTest.java @@ -0,0 +1,234 @@ +package dev.erichaag.develocity.processing; + +import dev.erichaag.develocity.api.Build; +import dev.erichaag.develocity.api.DevelocityClient; +import dev.erichaag.develocity.api.DevelocityClientStub; +import dev.erichaag.develocity.processing.cache.InMemoryCache; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static dev.erichaag.develocity.api.Builds.bazel; +import static dev.erichaag.develocity.api.Builds.gradle; +import static dev.erichaag.develocity.api.Builds.maven; +import static dev.erichaag.develocity.api.Builds.sbt; +import static java.time.Instant.ofEpochMilli; +import static java.util.stream.IntStream.range; +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class BuildProcessorTest { + + private final AtomicInteger overallBuildsEncountered = new AtomicInteger(); + private final AtomicInteger gradleBuildsEncountered = new AtomicInteger(); + private final AtomicInteger mavenBuildsEncountered = new AtomicInteger(); + private final AtomicInteger bazelBuildsEncountered = new AtomicInteger(); + private final AtomicInteger sbtBuildsEncountered = new AtomicInteger(); + + private final AtomicInteger discoveryStartedCalled = new AtomicInteger(); + private final AtomicInteger discoveryFinishedCalled = new AtomicInteger(); + private final AtomicInteger processingStartedCalled = new AtomicInteger(); + private final AtomicInteger cachedBuildCalled = new AtomicInteger(); + private final AtomicInteger fetchedBuildCalled = new AtomicInteger(); + private final AtomicInteger processingFinishedCalled = new AtomicInteger(); + + private final BuildListener countingBuildListener = BuildListener.builder() + .onBuild(__ -> overallBuildsEncountered.incrementAndGet()) + .onGradleBuild(__ -> gradleBuildsEncountered.incrementAndGet()) + .onMavenBuild(__ -> mavenBuildsEncountered.incrementAndGet()) + .onBazelBuild(__ -> bazelBuildsEncountered.incrementAndGet()) + .onSbtBuild(__ -> sbtBuildsEncountered.incrementAndGet()) + .build(); + + private final ProcessListener countingProcessListener = ProcessListener.builder() + .onDiscoveryStarted(__ -> discoveryStartedCalled.incrementAndGet()) + .onDiscoveryFinished(__ -> discoveryFinishedCalled.incrementAndGet()) + .onProcessingStarted(__ -> processingStartedCalled.incrementAndGet()) + .onCachedBuild(__ -> cachedBuildCalled.incrementAndGet()) + .onFetchedBuild(__ -> fetchedBuildCalled.incrementAndGet()) + .onProcessingFinished(__ -> processingFinishedCalled.incrementAndGet()) + .build(); + + private final List builds = List.of( + gradle(it -> it.availableAt(100L)), + maven (it -> it.availableAt(200L)), + bazel (it -> it.availableAt(300L)), + sbt (it -> it.availableAt(400L)), + gradle(it -> it.availableAt(500L)), + gradle(it -> it.availableAt(600L)), + maven (it -> it.availableAt(700L)), + gradle(it -> it.availableAt(800L)), + sbt (it -> it.availableAt(900L)), + gradle(it -> it.availableAt(1000L)), + bazel (it -> it.availableAt(1100L)), + gradle(it -> it.availableAt(1200L)) + ); + + private final DevelocityClient develocity = DevelocityClientStub.withBuilds(builds); + + private final BuildProcessorBuilder buildProcessor = BuildProcessor.forClient(develocity) + .register(countingBuildListener) + .register(countingProcessListener); + + @ParameterizedTest + @ValueSource(ints = {1, 2, 100}) + void givenBuildsSince0_whenProcessed_thenBuildsSince0AreProcessed(int maxBuildsPerRequest) { + buildProcessor + .withMaxBuildsPerRequest(maxBuildsPerRequest) + .build() + .process(ofEpochMilli(0)); + assertOverallBuildsEncountered(12); + assertGradleBuildsEncountered(6); + assertMavenBuildsEncountered(2); + assertBazelBuildsEncountered(2); + assertSbtBuildsEncountered(2); + } + + @Test + void givenBuildsSince500_whenProcessed_thenBuildsSince500AreProcessed() { + buildProcessor + .withMaxBuildsPerRequest(1) + .build() + .process(ofEpochMilli(500)); + assertOverallBuildsEncountered(8); + assertGradleBuildsEncountered(5); + assertMavenBuildsEncountered(1); + assertBazelBuildsEncountered(1); + assertSbtBuildsEncountered(1); + } + + @Test + void givenBuildsSince1200_whenProcessed_thenBuildsSince1200AreProcessed() { + buildProcessor + .withMaxBuildsPerRequest(1) + .build() + .process(ofEpochMilli(1200)); + assertOverallBuildsEncountered(1); + assertGradleBuildsEncountered(1); + assertMavenBuildsEncountered(0); + assertBazelBuildsEncountered(0); + assertSbtBuildsEncountered(0); + } + + @Test + void givenBuildsSince1201_whenProcessed_thenNoBuildsAreProcessed() { + buildProcessor + .withMaxBuildsPerRequest(1) + .build() + .process(ofEpochMilli(1201)); + assertOverallBuildsEncountered(0); + assertGradleBuildsEncountered(0); + assertMavenBuildsEncountered(0); + assertBazelBuildsEncountered(0); + assertSbtBuildsEncountered(0); + } + + @Test + void givenEmptyCache_whenProcessed_thenAllBuildsAreFetched() { + buildProcessor + .withMaxBuildsPerRequest(1) + .build() + .process(ofEpochMilli(0)); + assertDiscoveryStartedCalledOnce(); + assertDiscoveryFinishedCalledOnce(); + assertProcessingStartedCalledOnce(); + assertFetchedBuildCalled(12); + assertProcessingFinishedCalledOnce(); + } + + @Test + void givenSomeBuildsAreCached_whenProcessed_thenCachedBuildsAreReusedAndRemainingFetched() { + final var inMemoryCache = InMemoryCache.withDefaultSize(); + range(0, 3).forEach(i -> inMemoryCache.save(builds.get(i))); + inMemoryCache.save(builds.get(0)); + inMemoryCache.save(builds.get(1)); + inMemoryCache.save(builds.get(2)); + buildProcessor + .withMaxBuildsPerRequest(1) + .withProcessorCache(inMemoryCache) + .build() + .process(ofEpochMilli(0)); + assertDiscoveryStartedCalledOnce(); + assertDiscoveryFinishedCalledOnce(); + assertProcessingStartedCalledOnce(); + assertFetchedBuildCalled(9); + assertCachedBuildCalled(3); + assertProcessingFinishedCalledOnce(); + } + + @Test + void givenAllBuildsAreCached_whenProcessed_thenAllCachedBuildsAreReusedAndNoneFetched() { + final var inMemoryCache = InMemoryCache.withDefaultSize(); + builds.forEach(inMemoryCache::save); + buildProcessor + .withMaxBuildsPerRequest(1) + .withProcessorCache(inMemoryCache) + .build() + .process(ofEpochMilli(0)); + assertDiscoveryStartedCalledOnce(); + assertDiscoveryFinishedCalledOnce(); + assertProcessingStartedCalledOnce(); + assertFetchedBuildCalled(0); + assertCachedBuildCalled(12); + assertProcessingFinishedCalledOnce(); + } + + private void assertOverallBuildsEncountered(int expected) { + assertBuildsEncountered(expected, overallBuildsEncountered, "overall"); + } + + private void assertGradleBuildsEncountered(int expected) { + assertBuildsEncountered(expected, gradleBuildsEncountered, "Gradle"); + } + + private void assertMavenBuildsEncountered(int expected) { + assertBuildsEncountered(expected, mavenBuildsEncountered, "Maven"); + } + + private void assertBazelBuildsEncountered(int expected) { + assertBuildsEncountered(expected, bazelBuildsEncountered, "Bazel"); + } + + private void assertSbtBuildsEncountered(int expected) { + assertBuildsEncountered(expected, sbtBuildsEncountered, "sbt"); + } + + private static void assertBuildsEncountered(int expected, AtomicInteger actual, String type) { + assertEquals(expected, actual.get(), () -> "Expected " + expected + " " + type + " " + pluralize("build", expected) + " but encountered " + actual.get()); + } + + private void assertDiscoveryStartedCalledOnce() { + assertProcessListenerMethodCalled(1, discoveryStartedCalled, "discovery started"); + } + + private void assertDiscoveryFinishedCalledOnce() { + assertProcessListenerMethodCalled(1, discoveryFinishedCalled, "discovery finished"); + } + + private void assertProcessingStartedCalledOnce() { + assertProcessListenerMethodCalled(1, processingStartedCalled, "processing started"); + } + + private void assertCachedBuildCalled(int expected) { + assertProcessListenerMethodCalled(expected, cachedBuildCalled, "cached build"); + } + + private void assertFetchedBuildCalled(int expected) { + assertProcessListenerMethodCalled(expected, fetchedBuildCalled, "fetched build"); + } + + private void assertProcessingFinishedCalledOnce() { + assertProcessListenerMethodCalled(1, processingFinishedCalled, "processing finished"); + } + + private static void assertProcessListenerMethodCalled(int expected, AtomicInteger actual, String type) { + assertEquals(expected, actual.get(), () -> "Expected " + expected + " " + type + " " + pluralize("call", expected) + " but encountered " + actual.get()); + } + + private static String pluralize(String value, int i) { + return i == 1 ? value : value + "s"; + } + +} diff --git a/src/test/java/dev/erichaag/develocity/processing/cache/CompositeCacheTest.java b/src/test/java/dev/erichaag/develocity/processing/cache/CompositeCacheTest.java index 9052590..897c255 100644 --- a/src/test/java/dev/erichaag/develocity/processing/cache/CompositeCacheTest.java +++ b/src/test/java/dev/erichaag/develocity/processing/cache/CompositeCacheTest.java @@ -1,64 +1,53 @@ package dev.erichaag.develocity.processing.cache; -import org.junit.jupiter.api.BeforeEach; +import dev.erichaag.develocity.api.Build; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import java.nio.file.Path; - -import static dev.erichaag.develocity.api.Builds.gradleBuild; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static dev.erichaag.develocity.api.Builds.gradle; final class CompositeCacheTest extends AbstractCacheTest { - InMemoryCache inMemoryCache; - FileSystemCache fileSystemCache; + private static final String id = "foobarbazqux1"; + + private final InMemoryCache primaryCache = InMemoryCache.withDefaultSize(); + private final InMemoryCache secondaryCache = InMemoryCache.withDefaultSize(); - @BeforeEach - void beforeEach(@TempDir Path temporaryCacheDirectory) { - this.inMemoryCache = InMemoryCache.withDefaultSize(); - this.fileSystemCache = FileSystemCache.withStrategy(new PartitioningFileSystemCacheStrategy(temporaryCacheDirectory, 2)); - this.cache = CompositeCache.firstChecking(inMemoryCache) - .followedBy(fileSystemCache); + @Override + protected ProcessorCache createCache() { + return CompositeCache.firstChecking(primaryCache).followedBy(secondaryCache); } @Test - void whenSavedToCompositeCache_thenBuildIsSavedToAllCaches() { - final var id = "foobarbazqux1"; - final var savedBuild = gradleBuild(id); - cache.save(savedBuild); - final var inMemoryBuild = inMemoryCache.load(id); - final var fileSystemBuild = fileSystemCache.load(id); - assertTrue(inMemoryBuild.isPresent()); - assertEquals(savedBuild, inMemoryBuild.get()); - assertTrue(fileSystemBuild.isPresent()); - assertEquals(savedBuild, fileSystemBuild.get()); + void whenSavedToCompositeCache_thenBuildIsReplicatedToAllCaches() { + final var buildSavedToCompositeCache = whenBuildSaved(gradle(id)); + thenBuildIsRetrievedSuccessfully(buildSavedToCompositeCache, primaryCache.load(id)); + thenBuildIsRetrievedSuccessfully(buildSavedToCompositeCache, secondaryCache.load(id)); } @Test - void givenBuildExistsInMemoryButNotOnFileSystem_whenLoaded_thenBuildIsLoadedButBuildIsNotPersistedToFileSystem() { - final var id = "foobarbazqux1"; - final var inMemoryBuild = gradleBuild(id); - inMemoryCache.save(inMemoryBuild); - final var compositeBuild = cache.load(id); - final var fileSystemBuild = fileSystemCache.load(id); - assertTrue(compositeBuild.isPresent()); - assertEquals(inMemoryBuild, compositeBuild.get()); - assertTrue(fileSystemBuild.isEmpty()); + void givenBuildExistsInPrimaryCacheOnly_whenLoaded_thenBuildIsNotReplicatedToSecondaryCache() { + final var buildInPrimaryCache = givenBuildExistsInPrimaryCache(gradle(id)); + final var buildFromCompositeCache = whenBuildLoadedFromCache(id); + thenBuildIsRetrievedSuccessfully(buildInPrimaryCache, buildFromCompositeCache); + thenNoBuildIsRetrieved(secondaryCache.load(id)); } @Test - void givenBuildExistsOnFileSystemButNotInMemory_whenLoaded_thenBuildIsLoadedAndBuildIsPersistedToMemory() { - final var id = "foobarbazqux1"; - final var fileSystemBuild = gradleBuild(id); - fileSystemCache.save(fileSystemBuild); - final var compositeBuild = cache.load(id); - final var inMemoryBuild = inMemoryCache.load(id); - assertTrue(compositeBuild.isPresent()); - assertEquals(fileSystemBuild, compositeBuild.get()); - assertTrue(inMemoryBuild.isPresent()); - assertEquals(fileSystemBuild, inMemoryBuild.get()); + void givenBuildExistsInSecondaryCacheOnly_whenLoaded_thenBuildIsReplicatedToPrimaryCache() { + final var buildInSecondaryCache = givenBuildExistsInSecondaryCache(gradle(id)); + final var buildFromCompositeCache = whenBuildLoadedFromCache(id); + thenBuildIsRetrievedSuccessfully(buildInSecondaryCache, buildFromCompositeCache); + thenBuildIsRetrievedSuccessfully(buildInSecondaryCache, primaryCache.load(id)); + } + + private Build givenBuildExistsInPrimaryCache(Build build) { + primaryCache.save(build); + return build; + } + + private Build givenBuildExistsInSecondaryCache(Build build) { + secondaryCache.save(build); + return build; } } diff --git a/src/test/java/dev/erichaag/develocity/processing/cache/FileSystemCacheTest.java b/src/test/java/dev/erichaag/develocity/processing/cache/FileSystemCacheTest.java index 6a206ab..6ec6eb7 100644 --- a/src/test/java/dev/erichaag/develocity/processing/cache/FileSystemCacheTest.java +++ b/src/test/java/dev/erichaag/develocity/processing/cache/FileSystemCacheTest.java @@ -1,6 +1,5 @@ package dev.erichaag.develocity.processing.cache; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -13,26 +12,27 @@ final class FileSystemCacheTest extends AbstractCacheTest { - Path temporaryCacheDirectory; - PartitioningFileSystemCacheStrategy cacheStrategy; + private static final String id = "foobarbazqux1"; - @BeforeEach - void beforeEach(@TempDir Path temporaryCacheDirectory) { - this.temporaryCacheDirectory = temporaryCacheDirectory; + @TempDir private Path temporaryCacheDirectory; + + private PartitioningFileSystemCacheStrategy cacheStrategy; + + @Override + protected ProcessorCache createCache() { this.cacheStrategy = new PartitioningFileSystemCacheStrategy(temporaryCacheDirectory, 2); - this.cache = FileSystemCache.withStrategy(cacheStrategy); + return FileSystemCache.withStrategy(cacheStrategy); } @Test - void givenCorruptFile_whenLoaded_thenBuildIsNotLoadedFromCacheAndFileIsDeleted() throws IOException { - final var id = "foobarbazqux1"; + void givenCorruptCacheFile_whenLoaded_thenBuildIsNotRetrievedFromCacheAndFileIsDeleted() throws IOException { final var corruptCacheFile = cacheStrategy.getPath(id).toFile(); //noinspection ResultOfMethodCallIgnored corruptCacheFile.getParentFile().mkdirs(); Files.write(corruptCacheFile.toPath(), "corrupt".getBytes()); - final var cachedBuild = cache.load(id); - assertTrue(cachedBuild.isEmpty()); - assertFalse(corruptCacheFile.exists()); + final var cachedBuild = whenBuildLoadedFromCache(id); + assertTrue(cachedBuild.isEmpty(), "Expected no build to be loaded from the corrupt cache file"); + assertFalse(corruptCacheFile.exists(), "Expected the corrupt cache file to be deleted"); } } diff --git a/src/test/java/dev/erichaag/develocity/processing/cache/InMemoryCacheTest.java b/src/test/java/dev/erichaag/develocity/processing/cache/InMemoryCacheTest.java index fa86432..52080e7 100644 --- a/src/test/java/dev/erichaag/develocity/processing/cache/InMemoryCacheTest.java +++ b/src/test/java/dev/erichaag/develocity/processing/cache/InMemoryCacheTest.java @@ -1,23 +1,22 @@ package dev.erichaag.develocity.processing.cache; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static dev.erichaag.develocity.api.Builds.gradleBuild; +import static dev.erichaag.develocity.api.Builds.gradle; import static org.junit.jupiter.api.Assertions.assertTrue; final class InMemoryCacheTest extends AbstractCacheTest { - @BeforeEach - void beforeEach() { - this.cache = InMemoryCache.withDefaultSize(); + @Override + protected ProcessorCache createCache() { + return InMemoryCache.withDefaultSize(); } @Test void givenFullCache_whenSaved_thenEntryIsPurged() { - final var firstBuild = gradleBuild("foobarbazqux1"); - final var secondBuild = gradleBuild("foobarbazqux2"); - final var thirdBuild = gradleBuild("foobarbazqux3"); + final var firstBuild = gradle("foobarbazqux1"); + final var secondBuild = gradle("foobarbazqux2"); + final var thirdBuild = gradle("foobarbazqux3"); final var cache = InMemoryCache.withSize(2); cache.save(firstBuild); cache.save(secondBuild); diff --git a/src/testFixtures/java/dev/erichaag/develocity/api/Builds.java b/src/testFixtures/java/dev/erichaag/develocity/api/Builds.java index b8a9d8b..7b2110a 100644 --- a/src/testFixtures/java/dev/erichaag/develocity/api/Builds.java +++ b/src/testFixtures/java/dev/erichaag/develocity/api/Builds.java @@ -1,13 +1,31 @@ package dev.erichaag.develocity.api; import java.util.List; +import java.util.function.UnaryOperator; public final class Builds { - public static GradleBuild gradleBuild(String id) { + public static final String buildToolTypeGradle = "gradle"; + public static final String buildToolTypeMaven = "maven"; + public static final String buildToolTypeBazel = "bazel"; + public static final String buildToolTypeSbt = "sbt"; + + public static GradleBuild gradle() { + return (GradleBuild) Build.from(gradleApiBuild(null)); + } + + public static GradleBuild gradle(UnaryOperator modify) { + return (GradleBuild) Build.from(modify.apply(gradleApiBuild(null))); + } + + public static GradleBuild gradle(String id) { return (GradleBuild) Build.from(gradleApiBuild(id)); } + public static GradleBuild gradle(String id, UnaryOperator modify) { + return (GradleBuild) Build.from(modify.apply(gradleApiBuild(id))); + } + public static GradleAttributes gradleAttributes() { return new GradleAttributes() .buildDuration(100L) @@ -20,8 +38,12 @@ public static GradleAttributes gradleAttributes() { .tags(List.of("CI", "Linux", "main")); } + public static List gradleProjects() { + return List.of(new GradleProject().name("develocity-build-processor")); + } + @SuppressWarnings("unchecked") - public static GradleBuild gradleBuildWith(String id, Object... buildModels) { + public static GradleBuild gradleWith(String id, Object... buildModels) { final var models = new BuildModels(); for (final var buildModel : buildModels) { switch (buildModel) { @@ -37,13 +59,88 @@ public static GradleBuild gradleBuildWith(String id, Object... buildModels) { return (GradleBuild) Build.from(gradleApiBuild(id).models(models)); } + public static MavenBuild maven() { + return (MavenBuild) Build.from(mavenApiBuild(null)); + } + + public static MavenBuild maven(UnaryOperator modify) { + return (MavenBuild) Build.from(modify.apply(mavenApiBuild(null))); + } + + public static MavenBuild maven(String id) { + return (MavenBuild) Build.from(mavenApiBuild(id)); + } + + public static MavenBuild maven(String id, UnaryOperator modify) { + return (MavenBuild) Build.from(modify.apply(mavenApiBuild(id))); + } + + public static BazelBuild bazel() { + return (BazelBuild) Build.from(bazelApiBuild(null)); + } + + public static BazelBuild bazel(UnaryOperator modify) { + return (BazelBuild) Build.from(modify.apply(bazelApiBuild(null))); + } + + public static BazelBuild bazel(String id) { + return (BazelBuild) Build.from(bazelApiBuild(id)); + } + + public static BazelBuild bazel(String id, UnaryOperator modify) { + return (BazelBuild) Build.from(modify.apply(bazelApiBuild(id))); + } + + public static SbtBuild sbt() { + return (SbtBuild) Build.from(sbtApiBuild(null)); + } + + public static SbtBuild sbt(UnaryOperator modify) { + return (SbtBuild) Build.from(modify.apply(sbtApiBuild(null))); + } + + public static SbtBuild sbt(String id) { + return (SbtBuild) Build.from(sbtApiBuild(id)); + } + + public static SbtBuild sbt(String id, UnaryOperator modify) { + return (SbtBuild) Build.from(modify.apply(sbtApiBuild(id))); + } + private static ApiBuild gradleApiBuild(String id) { return new ApiBuild() .id(id) .availableAt(100L) .buildAgentVersion("3.17.5") - .buildToolType("gradle") + .buildToolType(buildToolTypeGradle) .buildToolVersion("8.6"); } + private static ApiBuild mavenApiBuild(String id) { + return new ApiBuild() + .id(id) + .availableAt(100L) + .buildAgentVersion("1.21.4") + .buildToolType(buildToolTypeMaven) + .buildToolVersion("3.9.8"); + } + + private static ApiBuild bazelApiBuild(String id) { + return new ApiBuild() + .id(id) + .availableAt(100L) + .buildAgentVersion("1.2") + .buildToolType(buildToolTypeBazel) + .buildToolVersion("7.2.1"); + } + + private static ApiBuild sbtApiBuild(String id) { + return new ApiBuild() + .id(id) + .availableAt(100L) + .buildAgentVersion("1.0.1") + .buildToolType(buildToolTypeSbt) + .buildToolVersion("1.10.0"); + } + } diff --git a/src/testFixtures/java/dev/erichaag/develocity/api/DevelocityClientStub.java b/src/testFixtures/java/dev/erichaag/develocity/api/DevelocityClientStub.java new file mode 100644 index 0000000..ed363f1 --- /dev/null +++ b/src/testFixtures/java/dev/erichaag/develocity/api/DevelocityClientStub.java @@ -0,0 +1,61 @@ +package dev.erichaag.develocity.api; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.IntStream; + +import static java.lang.Math.min; +import static java.util.Collections.emptyList; +import static java.util.Collections.reverseOrder; +import static java.util.Comparator.comparing; + +public final class DevelocityClientStub implements DevelocityClient { + + private static final int defaultMaxBuilds = 100; + + private final List builds; + private final Map buildsById = new HashMap<>(); + + private DevelocityClientStub(List builds) { + this.builds = builds.stream().sorted(reverseOrder(comparing(Build::getAvailableAt))).toList(); + builds.forEach(it -> { + if (buildsById.containsKey(it.getId())) { + throw new RuntimeException("Duplicate build ID: " + it.getId()); + } + buildsById.put(it.getId(), it); + }); + } + + public static DevelocityClientStub withBuilds(Build... builds) { + return withBuilds(List.of(builds)); + } + + public static DevelocityClientStub withBuilds(List builds) { + IntStream.range(0, builds.size()) + .filter(i -> builds.get(i).getId() == null) + .forEach(i -> builds.get(i).getBuild().setId("foobarbazqux" + i)); + return new DevelocityClientStub(builds); + } + + @Override + public Optional getBuild(String id, Set buildModels) { + return builds.stream().filter(it -> it.getId().equals(id)).findFirst(); + } + + @Override + public List getBuilds(String query, Integer maxBuilds, String fromBuild, Set buildModels) { + maxBuilds = maxBuilds == null ? defaultMaxBuilds : maxBuilds; + if (fromBuild == null) { + return builds.subList(0, min(builds.size(), maxBuilds)); + } + final var buildIndex = builds.indexOf(buildsById.get(fromBuild)); + if (buildIndex == -1 || buildIndex == builds.size() - 1) { + return emptyList(); + } + return builds.subList(buildIndex + 1, min(builds.size(), buildIndex + maxBuilds + 1)); + } + +} diff --git a/src/testFixtures/java/dev/erichaag/develocity/processing/cache/AbstractCacheTest.java b/src/testFixtures/java/dev/erichaag/develocity/processing/cache/AbstractCacheTest.java index 938f9cc..00b1327 100644 --- a/src/testFixtures/java/dev/erichaag/develocity/processing/cache/AbstractCacheTest.java +++ b/src/testFixtures/java/dev/erichaag/develocity/processing/cache/AbstractCacheTest.java @@ -1,34 +1,160 @@ package dev.erichaag.develocity.processing.cache; +import dev.erichaag.develocity.api.Build; +import dev.erichaag.develocity.api.BuildModel; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Optional; + import static dev.erichaag.develocity.api.BuildModel.GRADLE_ATTRIBUTES; import static dev.erichaag.develocity.api.BuildModel.GRADLE_BUILD_CACHE_PERFORMANCE; import static dev.erichaag.develocity.api.Builds.gradleAttributes; -import static dev.erichaag.develocity.api.Builds.gradleBuildWith; +import static dev.erichaag.develocity.api.Builds.gradleProjects; +import static dev.erichaag.develocity.api.Builds.gradleWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * An abstract base class for testing {@link ProcessorCache} implementations. + * + *

This class provides a set of common test scenarios and helper methods to + * streamline the testing of different cache implementations. Subclasses must + * implement the {@link #createCache()} method to provide a concrete cache + * instance for testing. + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public abstract class AbstractCacheTest { - ProcessorCache cache; + private static final String id = "foobarbazqux1"; + + private ProcessorCache cache; + + /** + * Creates and returns a new instance of the {@link ProcessorCache} + * implementation under test. + * + * @return a new {@link ProcessorCache} instance + */ + protected abstract ProcessorCache createCache(); + + /** + * Returns the {@link ProcessorCache} instance being tested. + * + *

Implementations of this abstract class should favor using the + * provided helper methods and only result to this method in the event a + * suitable helper method does not exist. + * + * @return the {@link ProcessorCache} instance + */ + protected ProcessorCache cache() { + return cache; + } + + /** + * Prepares the cache for a test scenario where a build already exists in + * the cache. + * + *

This method saves the given build to the cache and returns it for + * further use. + * + * @param build the build to save in the cache + * @return the saved build + */ + protected Build givenBuildExistsInCache(Build build) { + cache.save(build); + return build; + } + + /** + * Saves a build to the cache as part of a test scenario. + * + *

Use this method when you want to test the cache's behavior after a + * build is saved. + * + * @param build the build to save in the cache + * @return the saved build + */ + protected Build whenBuildSaved(Build build) { + cache.save(build); + return build; + } + + /** + * Performs a load operation on the cache, requesting the build with the + * given ID and models. + * + *

Use this method to test the cache's behavior in response to a load + * request. + * + * @param id the ID of the build to load + * @param buildModels the models associated with the build + * @return an Optional containing the loaded build, or empty if not found + */ + protected Optional whenBuildLoadedFromCache(String id, BuildModel... buildModels) { + return cache.load(id, buildModels); + } + + /** + * Asserts that a build was retrieved successfully from the cache, matching + * the expected build. + * + * @param expected the expected build + * @param actual the Optional containing the actual build (or empty if not + * found) + */ + protected void thenBuildIsRetrievedSuccessfully(Build expected, Optional actual) { + assertTrue(actual.isPresent(), "Expected a build to be retrieved successfully, but there wasn't"); + assertEquals(expected, actual.get()); + } + + /** + * Asserts that no build was retrieved from the cache. + * + * @param actual the Optional that should be empty + */ + protected void thenNoBuildIsRetrieved(Optional actual) { + assertTrue(actual.isEmpty(), "Expected no build to be retrieved, but there was"); + } + + @BeforeEach + void beforeEach() { + this.cache = createCache(); + } + + @Test + void givenBuildExistsInCache_whenLoaded_thenBuildIsRetrievedSuccessfully() { + final var buildInCache = givenBuildExistsInCache(gradleWith(id, gradleAttributes())); + final var buildFromCache = whenBuildLoadedFromCache(id, GRADLE_ATTRIBUTES); + thenBuildIsRetrievedSuccessfully(buildInCache, buildFromCache); + } + + @Test + void givenBuildExistsInCache_whenLoadedButForDifferentModels_thenNoBuildIsRetrieved() { + givenBuildExistsInCache(gradleWith(id, gradleAttributes())); + final var buildFromCache = whenBuildLoadedFromCache(id, GRADLE_BUILD_CACHE_PERFORMANCE); + thenNoBuildIsRetrieved(buildFromCache); + } + + @Test + void givenBuildExistsInCacheWithMultipleModels_whenLoadedForOnlyOneModel_thenBuildIsRetrievedSuccessfully() { + final var buildInCache = givenBuildExistsInCache(gradleWith(id, gradleAttributes(), gradleProjects())); + final var buildFromCache = whenBuildLoadedFromCache(id, GRADLE_ATTRIBUTES); + thenBuildIsRetrievedSuccessfully(buildInCache, buildFromCache); + } @Test - void givenCachedBuild_whenLoaded_thenBuildIsLoadedFromCache() { - final var id = "foobarbazqux1"; - final var savedBuild = gradleBuildWith(id, gradleAttributes()); - cache.save(savedBuild); - final var cachedBuild = cache.load(id, GRADLE_ATTRIBUTES); - assertTrue(cachedBuild.isPresent()); - assertEquals(savedBuild, cachedBuild.get()); + void givenBuildExistsInCache_whenBuildSavedWithSameId_thenPreviousBuildIsOverwritten() { + givenBuildExistsInCache(gradleWith(id)); + final var newBuildInCache = whenBuildSaved(gradleWith(id, gradleAttributes())); + final var buildFromCache = whenBuildLoadedFromCache(id); + thenBuildIsRetrievedSuccessfully(newBuildInCache, buildFromCache); } @Test - void givenCachedBuild_whenLoadedForDifferentModels_thenBuildIsNotLoadedFromCache() { - final var id = "foobarbazqux1"; - cache.save(gradleBuildWith(id, gradleAttributes())); - final var cachedBuild = cache.load(id, GRADLE_BUILD_CACHE_PERFORMANCE); - assertTrue(cachedBuild.isEmpty()); + void givenBuildDoesNotExistInCache_whenLoaded_thenNoBuildIsRetrieved() { + final var buildFromCache = whenBuildLoadedFromCache(id); + thenNoBuildIsRetrieved(buildFromCache); } }