Compare commits

...

2 commits

Author SHA1 Message Date
Geert Bevin dffe1cf6df Count classpath caching test requests only after the prefetcher settles
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run
2026-07-26 08:50:40 -04:00
Geert Bevin 3ced706fa7 Cache the resolved classpath jars for isolated dependency and extension trees 2026-07-26 08:23:19 -04:00
3 changed files with 454 additions and 10 deletions

View file

@ -1669,6 +1669,9 @@ public class BaseProject extends BuildExecutor {
return dependencies;
}
private final Map<String, List<File>> classpathJarsMemo_ = new HashMap<>();
private DependencySet resolvedExtensions_ = null;
/**
* Creates the version resolution for a dependency scope, applying the
* version overrides from the properties and the BOMs that are
@ -1708,16 +1711,39 @@ public class BaseProject extends BuildExecutor {
* @since 2.4.0
*/
public List<File> dependencyClasspathJars(Scope scope, String groupId, String artifactId) {
var coordinate = groupId + ":" + artifactId;
var memo_key = "dependency." + scope + "." + coordinate;
var memoized = classpathJarsMemo_.get(memo_key);
if (memoized != null) {
return memoized;
}
var declared = dependencies().scope(scope).get(new Dependency(groupId, artifactId));
if (declared == null) {
throw new IllegalArgumentException("Dependency '" + groupId + ":" + artifactId + "' isn't declared in the " + scope + " scope.");
throw new IllegalArgumentException("Dependency '" + coordinate + "' isn't declared in the " + scope + " scope.");
}
var dir = scopeLibDirectory(scope);
var cache = new BldCache(libBldDirectory(), new VersionResolution(properties()));
cache.cacheDependenciesHash(repositories(), dependencies());
if (cache.isDependenciesHashValid()) {
var cached = cache.getCachedDependencyClasspath(scope, coordinate);
if (cached != null) {
var jars = cached.stream().map(name -> new File(dir, name)).toList();
classpathJarsMemo_.put(memo_key, jars);
return jars;
}
}
var resolver = new DependencyResolver(versionResolution(scope), artifactRetriever(), repositories(), declared);
var dir = scopeLibDirectory(scope);
return resolver.getAllDependencies(Scope.compile, Scope.runtime).stream()
var jars = resolver.getAllDependencies(Scope.compile, Scope.runtime).stream()
.map(dependency -> new File(dir, dependency.toFileName()))
.toList();
cache.cacheDependencyClasspath(scope, coordinate, jars.stream().map(File::getName).toList());
cache.writeCache();
classpathJarsMemo_.put(memo_key, jars);
return jars;
}
private File scopeLibDirectory(Scope scope) {
@ -1751,6 +1777,13 @@ public class BaseProject extends BuildExecutor {
* @since 2.4.0
*/
public List<File> extensionClasspathJars(String groupId, String artifactId) {
var coordinate = groupId + ":" + artifactId;
var memo_key = "extension." + coordinate;
var memoized = classpathJarsMemo_.get(memo_key);
if (memoized != null) {
return memoized;
}
var wrapper = new Wrapper();
wrapper.currentDir(workDirectory());
try {
@ -1762,25 +1795,45 @@ public class BaseProject extends BuildExecutor {
var properties = new HierarchicalProperties().parent(BuildExecutor.setupProperties(workDirectory()));
properties.putAll(wrapper.wrapperProperties());
var resolution = new VersionResolution(properties);
var cache = new BldCache(libBldDirectory(), resolution);
cache.cacheExtensionsHash(wrapper.repositories(), wrapper.extensions());
if (cache.isExtensionsHashValid()) {
var cached = cache.getCachedExtensionClasspath(coordinate);
if (cached != null) {
var jars = cached.stream().map(name -> new File(libBldDirectory(), name)).toList();
classpathJarsMemo_.put(memo_key, jars);
return jars;
}
}
var repositories = new ArrayList<Repository>();
for (var repository : wrapper.repositories()) {
repositories.add(Repository.resolveRepository(properties, repository));
}
var resolution = new VersionResolution(properties);
var extensions = wrapper.extensions().stream().map(Dependency::parse).toList();
var resolved = new ParallelDependencyResolver(resolution, artifactRetriever(), repositories)
.resolveAllDependencies(extensions, Scope.compile, Scope.runtime);
// the resolved set of all the extensions is reused across
// lookups of different dependencies in the same build
if (resolvedExtensions_ == null) {
var extensions = wrapper.extensions().stream().map(Dependency::parse).toList();
resolvedExtensions_ = new ParallelDependencyResolver(resolution, artifactRetriever(), repositories)
.resolveAllDependencies(extensions, Scope.compile, Scope.runtime);
}
var dependency = resolved.get(new Dependency(groupId, artifactId));
var dependency = resolvedExtensions_.get(new Dependency(groupId, artifactId));
if (dependency == null) {
throw new IllegalArgumentException("Dependency '" + groupId + ":" + artifactId + "' isn't part of the extensions of this project.");
throw new IllegalArgumentException("Dependency '" + coordinate + "' isn't part of the extensions of this project.");
}
var resolver = new DependencyResolver(resolution, artifactRetriever(), repositories, dependency);
return resolver.getAllDependencies(Scope.compile, Scope.runtime).stream()
var jars = resolver.getAllDependencies(Scope.compile, Scope.runtime).stream()
.map(d -> new File(libBldDirectory(), d.toFileName()))
.toList();
cache.cacheExtensionClasspath(coordinate, jars.stream().map(File::getName).toList());
cache.writeCache();
classpathJarsMemo_.put(memo_key, jars);
return jars;
}
/**

View file

@ -5,6 +5,7 @@
package rife.bld;
import rife.bld.dependencies.DependencyScopes;
import rife.bld.dependencies.Scope;
import rife.bld.dependencies.Repository;
import rife.bld.dependencies.VersionResolution;
import rife.bld.wrapper.Wrapper;
@ -40,12 +41,15 @@ public class BldCache {
private static final String WRAPPER_PROPERTIES_HASH = Wrapper.WRAPPER_PROPERTIES + PROPERTY_SUFFIX_HASH;
private static final String BLD_BUILD_HASH = "bld-build" + PROPERTY_SUFFIX_HASH;
private static final String PROPERTY_SUFFIX_CLASSPATH = ".classpath.";
private static final String PROPERTY_EXTENSIONS_PREFIX = "bld.extensions";
private static final String PROPERTY_EXTENSIONS_HASH = PROPERTY_EXTENSIONS_PREFIX + PROPERTY_SUFFIX_HASH;
private static final String PROPERTY_EXTENSIONS_LOCAL = PROPERTY_EXTENSIONS_PREFIX + PROPERTY_SUFFIX_LOCAL;
private static final String PROPERTY_EXTENSIONS_DOWNLOAD_SOURCES = PROPERTY_EXTENSIONS_PREFIX + PROPERTY_SUFFIX_DOWNLOAD_SOURCES;
private static final String PROPERTY_EXTENSIONS_DOWNLOAD_JAVADOC = PROPERTY_EXTENSIONS_PREFIX + PROPERTY_SUFFIX_DOWNLOAD_JAVADOC;
private static final String PROPERTY_EXTENSIONS_DEPENDENCY_TREE = PROPERTY_EXTENSIONS_PREFIX + PROPERTY_SUFFIX_DEPENDENCY_TREE;
private static final String PROPERTY_EXTENSIONS_CLASSPATH_PREFIX = PROPERTY_EXTENSIONS_PREFIX + PROPERTY_SUFFIX_CLASSPATH;
private static final String PROPERTY_DEPENDENCIES_PREFIX = "bld.dependencies";
private static final String PROPERTY_DEPENDENCIES_HASH = PROPERTY_DEPENDENCIES_PREFIX + PROPERTY_SUFFIX_HASH;
@ -55,6 +59,7 @@ public class BldCache {
private static final String PROPERTY_DEPENDENCIES_PROVIDED_DEPENDENCY_TREE = PROPERTY_DEPENDENCIES_PREFIX + ".provided" + PROPERTY_SUFFIX_DEPENDENCY_TREE;
private static final String PROPERTY_DEPENDENCIES_RUNTIME_DEPENDENCY_TREE = PROPERTY_DEPENDENCIES_PREFIX + ".runtime" + PROPERTY_SUFFIX_DEPENDENCY_TREE;
private static final String PROPERTY_DEPENDENCIES_TEST_DEPENDENCY_TREE = PROPERTY_DEPENDENCIES_PREFIX + ".test" + PROPERTY_SUFFIX_DEPENDENCY_TREE;
private static final String PROPERTY_DEPENDENCIES_CLASSPATH_PREFIX = PROPERTY_DEPENDENCIES_PREFIX + PROPERTY_SUFFIX_CLASSPATH;
private final File cacheDir_;
private final VersionResolution resolution_;
@ -64,6 +69,8 @@ public class BldCache {
private List<File> extensionsLocalArtifacts_;
private String extensionsDependencyTree_;
private String dependenciesHash_;
private final java.util.Map<String, String> extensionClasspaths_ = new java.util.LinkedHashMap<>();
private final java.util.Map<String, String> dependencyClasspaths_ = new java.util.LinkedHashMap<>();
private Boolean dependenciesDownloadSources_;
private Boolean dependenciesDownloadJavadocs_;
private String dependenciesCompileDependencyTree_;
@ -477,6 +484,76 @@ public class BldCache {
return properties;
}
/**
* Sets the resolved classpath jar names of a single extension
* dependency, to store with {@link #writeCache()}.
* <p>
* The names are only meaningful while the extensions hash is valid,
* check {@link #isExtensionsHashValid()} before using them.
*
* @param coordinate the {@code groupId:artifactId} of the dependency
* @param jarNames the jar file names of its transitive classpath
* @see #getCachedExtensionClasspath
* @since 2.4.0
*/
public void cacheExtensionClasspath(String coordinate, List<String> jarNames) {
extensionClasspaths_.put(coordinate, String.join("\n", jarNames));
}
/**
* Retrieves the previously stored classpath jar names of a single
* extension dependency.
*
* @param coordinate the {@code groupId:artifactId} of the dependency
* @return the stored jar file names; or {@code null} when nothing was
* stored for the coordinate
* @since 2.4.0
* @see #cacheExtensionClasspath
*/
public List<String> getCachedExtensionClasspath(String coordinate) {
var stored = hashProperties().getProperty(PROPERTY_EXTENSIONS_CLASSPATH_PREFIX + coordinate);
if (stored == null || stored.isEmpty()) {
return null;
}
return List.of(stored.split("\n"));
}
/**
* Sets the resolved classpath jar names of a single dependency in a
* scope, to store with {@link #writeCache()}.
* <p>
* The names are only meaningful while the dependencies hash is valid,
* check {@link #isDependenciesHashValid()} before using them.
*
* @param scope the scope the dependency is declared in
* @param coordinate the {@code groupId:artifactId} of the dependency
* @param jarNames the jar file names of its transitive classpath
* @since 2.4.0
* @see #getCachedDependencyClasspath
*/
public void cacheDependencyClasspath(Scope scope, String coordinate, List<String> jarNames) {
dependencyClasspaths_.put(scope + "." + coordinate, String.join("\n", jarNames));
}
/**
* Retrieves the previously stored classpath jar names of a single
* dependency in a scope.
*
* @param scope the scope the dependency is declared in
* @param coordinate the {@code groupId:artifactId} of the dependency
* @return the stored jar file names; or {@code null} when nothing was
* stored for the coordinate
* @since 2.4.0
* @see #cacheDependencyClasspath
*/
public List<String> getCachedDependencyClasspath(Scope scope, String coordinate) {
var stored = hashProperties().getProperty(PROPERTY_DEPENDENCIES_CLASSPATH_PREFIX + scope + "." + coordinate);
if (stored == null || stored.isEmpty()) {
return null;
}
return List.of(stored.split("\n"));
}
/**
* Writes the state of this {@code BldCache} instance to disk.
*
@ -490,7 +567,9 @@ public class BldCache {
if (!extensionsHash_.equals(properties.get(PROPERTY_EXTENSIONS_HASH))) {
properties.put(PROPERTY_EXTENSIONS_HASH, extensionsHash_);
properties.remove(PROPERTY_EXTENSIONS_DEPENDENCY_TREE);
properties.keySet().removeIf(key -> key.toString().startsWith(PROPERTY_EXTENSIONS_CLASSPATH_PREFIX));
}
extensionClasspaths_.forEach((coordinate, jars) -> properties.put(PROPERTY_EXTENSIONS_CLASSPATH_PREFIX + coordinate, jars));
if (extensionsDependencyTree_ != null) {
properties.put(PROPERTY_EXTENSIONS_DEPENDENCY_TREE, extensionsDependencyTree_);
@ -525,7 +604,9 @@ public class BldCache {
properties.remove(PROPERTY_DEPENDENCIES_PROVIDED_DEPENDENCY_TREE);
properties.remove(PROPERTY_DEPENDENCIES_RUNTIME_DEPENDENCY_TREE);
properties.remove(PROPERTY_DEPENDENCIES_TEST_DEPENDENCY_TREE);
properties.keySet().removeIf(key -> key.toString().startsWith(PROPERTY_DEPENDENCIES_CLASSPATH_PREFIX));
}
dependencyClasspaths_.forEach((coordinate, jars) -> properties.put(PROPERTY_DEPENDENCIES_CLASSPATH_PREFIX + coordinate, jars));
if (dependenciesCompileDependencyTree_ != null) {
properties.put(PROPERTY_DEPENDENCIES_COMPILE_DEPENDENCY_TREE, dependenciesCompileDependencyTree_);

View file

@ -426,14 +426,324 @@ public class TestClasspathJars {
FileUtils.writeString(properties, new File(lib_bld, "bld-wrapper.properties"));
}
@Test
void testDependencyClasspathJarsCaching() throws Exception {
var requests = new java.util.concurrent.atomic.AtomicInteger();
var server = createArtifactServer(Map.of(
"tool:1.0.0", pom("tool", "1.0.0", dependency("liba", "1.1.0")),
"liba:1.1.0", pom("liba", "1.1.0", ""),
"tool:2.0.0", pom("tool", "2.0.0", "")), requests);
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
project.dependencies().scope(provided)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
var jars = project.dependencyClasspathJars(provided, "com.example", "tool");
var resolution_requests = settledRequests(requests);
assertTrue(resolution_requests > 0);
// a repeated call in the same build resolves nothing
assertEquals(jars, project.dependencyClasspathJars(provided, "com.example", "tool"));
assertEquals(resolution_requests, requests.get());
// a fresh project instance reads the persisted cache instead
// of resolving, like a new build invocation would
var fresh = new JarsProject(tmp, serverRepository(server));
fresh.dependencies().scope(provided)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
assertEquals(jars, fresh.dependencyClasspathJars(provided, "com.example", "tool"));
assertEquals(resolution_requests, requests.get());
// changing the declared version invalidates the cache
var changed = new JarsProject(tmp, serverRepository(server));
changed.dependencies().scope(provided)
.include(new Dependency("com.example", "tool", new VersionNumber(2, 0, 0)));
assertEquals(List.of("tool-2.0.0.jar"),
changed.dependencyClasspathJars(provided, "com.example", "tool").stream().map(File::getName).toList());
assertTrue(requests.get() > resolution_requests);
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testExtensionClasspathJarsCaching() throws Exception {
var requests = new java.util.concurrent.atomic.AtomicInteger();
var server = createArtifactServer(Map.of(
"ext:1.0.0", pom("ext", "1.0.0", dependency("tool", "1.0.0")),
"tool:1.0.0", pom("tool", "1.0.0", dependency("liba", "1.1.0")),
"liba:1.1.0", pom("liba", "1.1.0", "")), requests);
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
writeWrapperProperties(project, server, "com.example:ext:1.0.0");
var jars = project.extensionClasspathJars("com.example", "tool");
var resolution_requests = settledRequests(requests);
assertTrue(resolution_requests > 0);
// repeated lookups in the same build resolve nothing, also
// for another dependency out of the same extension universe
assertEquals(jars, project.extensionClasspathJars("com.example", "tool"));
assertEquals(resolution_requests, requests.get());
var liba_jars = project.extensionClasspathJars("com.example", "liba");
assertEquals(List.of("liba-1.1.0.jar"), liba_jars.stream().map(File::getName).toList());
var universe_requests = settledRequests(requests);
assertEquals(resolution_requests, universe_requests);
// a fresh project instance reads the persisted cache
var fresh = new JarsProject(tmp, serverRepository(server));
assertEquals(jars, fresh.extensionClasspathJars("com.example", "tool"));
assertEquals(universe_requests, requests.get());
// changing the declared extensions invalidates the cache
writeWrapperProperties(project, server, "com.example:tool:1.0.0");
var changed = new JarsProject(tmp, serverRepository(server));
assertEquals(List.of("tool-1.0.0.jar", "liba-1.1.0.jar"),
changed.extensionClasspathJars("com.example", "tool").stream().map(File::getName).toList());
assertTrue(requests.get() > universe_requests);
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testDependencyClasspathJarsBomChangeInvalidates() throws Exception {
var requests = new java.util.concurrent.atomic.AtomicInteger();
var server = createArtifactServer(Map.of(
"bom1:1.0.0", bomPom("bom1", "1.0.0", dependency("libb", "1.5.0")),
"bom1:2.0.0", bomPom("bom1", "2.0.0", dependency("libb", "2.0.0")),
"tool:1.0.0", pom("tool", "1.0.0", dependency("libb", "3.0.0")),
"libb:1.5.0", pom("libb", "1.5.0", ""),
"libb:2.0.0", pom("libb", "2.0.0", ""),
"libb:3.0.0", pom("libb", "3.0.0", "")), requests);
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
project.dependencies().scope(test)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
assertEquals(List.of("tool-1.0.0.jar", "libb-1.5.0.jar"),
project.dependencyClasspathJars(test, "com.example", "tool").stream().map(File::getName).toList());
var warm_requests = requests.get();
// a different BOM version changes the hash and the pinned jar
var changed = new JarsProject(tmp, serverRepository(server));
changed.dependencies().scope(test)
.include(new Bom("com.example", "bom1", new VersionNumber(2, 0, 0)))
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
assertEquals(List.of("tool-1.0.0.jar", "libb-2.0.0.jar"),
changed.dependencyClasspathJars(test, "com.example", "tool").stream().map(File::getName).toList());
assertTrue(requests.get() > warm_requests);
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testDependencyClasspathJarsOverrideChangeInvalidates() throws Exception {
var requests = new java.util.concurrent.atomic.AtomicInteger();
var server = createArtifactServer(Map.of(
"tool:1.0.0", pom("tool", "1.0.0", dependency("libb", "2.0.0")),
"libb:2.0.0", pom("libb", "2.0.0", ""),
"libb:2.5.0", pom("libb", "2.5.0", "")), requests);
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
project.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
assertEquals(List.of("tool-1.0.0.jar", "libb-2.0.0.jar"),
project.dependencyClasspathJars(test, "com.example", "tool").stream().map(File::getName).toList());
var warm_requests = requests.get();
// a version override changes the hash and the resolved jar
var overridden = new JarsProject(tmp, serverRepository(server));
overridden.properties().put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:libb:2.5.0");
overridden.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
assertEquals(List.of("tool-1.0.0.jar", "libb-2.5.0.jar"),
overridden.dependencyClasspathJars(test, "com.example", "tool").stream().map(File::getName).toList());
assertTrue(requests.get() > warm_requests);
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testDependencyClasspathJarsRepositoryChangeInvalidates() throws Exception {
var requests1 = new java.util.concurrent.atomic.AtomicInteger();
var requests2 = new java.util.concurrent.atomic.AtomicInteger();
var poms = Map.of("tool:1.0.0", pom("tool", "1.0.0", ""));
var server1 = createArtifactServer(poms, requests1);
var server2 = createArtifactServer(poms, requests2);
server1.start();
server2.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server1));
project.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
project.dependencyClasspathJars(test, "com.example", "tool");
assertTrue(requests1.get() > 0);
// a different repository list changes the hash, the second
// server sees the resolution instead of the cache answering
var moved = new JarsProject(tmp, serverRepository(server2));
moved.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
moved.dependencyClasspathJars(test, "com.example", "tool");
assertTrue(requests2.get() > 0);
} finally {
server1.stop(0);
server2.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testDependencyClasspathJarsScopeKeysIsolated() throws Exception {
var requests = new java.util.concurrent.atomic.AtomicInteger();
var server = createArtifactServer(Map.of(
"tool:1.0.0", pom("tool", "1.0.0", ""),
"tool:2.0.0", pom("tool", "2.0.0", "")), requests);
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
project.dependencies().scope(provided)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
project.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(2, 0, 0)));
// the same coordinate is cached per scope, with its own version
assertEquals(List.of("tool-1.0.0.jar"),
project.dependencyClasspathJars(provided, "com.example", "tool").stream().map(File::getName).toList());
assertEquals(List.of("tool-2.0.0.jar"),
project.dependencyClasspathJars(test, "com.example", "tool").stream().map(File::getName).toList());
var warm_requests = settledRequests(requests);
var fresh = new JarsProject(tmp, serverRepository(server));
fresh.dependencies().scope(provided)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
fresh.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(2, 0, 0)));
assertEquals(List.of("tool-1.0.0.jar"),
fresh.dependencyClasspathJars(provided, "com.example", "tool").stream().map(File::getName).toList());
assertEquals(List.of("tool-2.0.0.jar"),
fresh.dependencyClasspathJars(test, "com.example", "tool").stream().map(File::getName).toList());
assertEquals(warm_requests, requests.get());
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testClasspathCacheCoexistsWithDependencyTrees() throws Exception {
var requests = new java.util.concurrent.atomic.AtomicInteger();
var server = createArtifactServer(Map.of(
"tool:1.0.0", pom("tool", "1.0.0", "")), requests);
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
project.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
project.dependencyClasspathJars(test, "com.example", "tool");
var warm_requests = settledRequests(requests);
// another BldCache user writing to the same file with the same
// hash preserves the classpath entries
var cache = new BldCache(project.libBldDirectory(), new VersionResolution(project.properties()));
cache.cacheDependenciesHash(project.repositories(), project.dependencies());
assertTrue(cache.isDependenciesHashValid());
cache.cacheDependenciesTestDependencyTree("tree placeholder");
cache.writeCache();
assertEquals("tree placeholder", cache.getCachedDependenciesTestDependencyTree());
var fresh = new JarsProject(tmp, serverRepository(server));
fresh.dependencies().scope(test)
.include(new Dependency("com.example", "tool", new VersionNumber(1, 0, 0)));
fresh.dependencyClasspathJars(test, "com.example", "tool");
assertEquals(warm_requests, requests.get());
// and a classpath write with the same hash preserves the tree
fresh.dependencyClasspathJars(test, "com.example", "tool");
assertEquals("tree placeholder", cache.getCachedDependenciesTestDependencyTree());
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
@Test
void testExtensionClasspathJarsUndeclaredAfterCached() throws Exception {
var server = createArtifactServer(Map.of(
"ext:1.0.0", pom("ext", "1.0.0", dependency("tool", "1.0.0")),
"tool:1.0.0", pom("tool", "1.0.0", ""),
"other:1.0.0", pom("other", "1.0.0", "")));
server.start();
var tmp = Files.createTempDirectory("classpathjars").toFile();
try {
var project = new JarsProject(tmp, serverRepository(server));
writeWrapperProperties(project, server, "com.example:ext:1.0.0");
assertEquals(1, project.extensionClasspathJars("com.example", "tool").size());
// the extension that brought the tool in is replaced, a stale
// cache entry must not keep answering for it
writeWrapperProperties(project, server, "com.example:other:1.0.0");
var changed = new JarsProject(tmp, serverRepository(server));
var exception = assertThrows(IllegalArgumentException.class,
() -> changed.extensionClasspathJars("com.example", "tool"));
assertTrue(exception.getMessage().contains("isn't part of the extensions"));
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
}
}
// the POM prefetcher can still have a speculative request in flight
// when resolution returns, count requests only after they settle
private static int settledRequests(java.util.concurrent.atomic.AtomicInteger requests)
throws InterruptedException {
var last = requests.get();
var stable_since = System.currentTimeMillis();
while (System.currentTimeMillis() - stable_since < 300) {
Thread.sleep(50);
var current = requests.get();
if (current != last) {
last = current;
stable_since = System.currentTimeMillis();
}
}
return last;
}
private static Repository serverRepository(HttpServer server) {
return new Repository("http://localhost:" + server.getAddress().getPort() + "/");
}
private static HttpServer createArtifactServer(Map<String, String> poms)
throws IOException {
return createArtifactServer(poms, new java.util.concurrent.atomic.AtomicInteger());
}
private static HttpServer createArtifactServer(Map<String, String> poms, java.util.concurrent.atomic.AtomicInteger requests)
throws IOException {
var server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
server.createContext("/", exchange -> {
requests.incrementAndGet();
var segments = exchange.getRequestURI().getPath().split("/");
var filename = segments[segments.length - 1];
byte[] body = null;