diff --git a/java/src/processing/mode/java/pdex/util/RuntimePathBuilder.java b/java/src/processing/mode/java/pdex/util/RuntimePathBuilder.java new file mode 100644 index 000000000..f3bc609e0 --- /dev/null +++ b/java/src/processing/mode/java/pdex/util/RuntimePathBuilder.java @@ -0,0 +1,685 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* +Part of the Processing project - http://processing.org +Copyright (c) 2012-19 The Processing Foundation + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +package processing.mode.java.pdex.util; + +import com.google.classpath.ClassPathFactory; +import processing.app.*; +import processing.mode.java.JavaMode; +import processing.mode.java.pdex.ImportStatement; +import processing.mode.java.pdex.PreprocessedSketch; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +/** + * Builder which generates runtime paths using a series of caches. + * + *

+ * The runtime path is dependent on the java runtime, libraries, code folder contents and is used + * both to determine classpath in sketch execution and to determine import suggestions within the + * editor. This builder determines those paths using a mixture of inputs (modules, jars, etc) + * and manages when those paths need to be recalculated (like with addition of a new library). + * Note that this is a wrapper around com.google.classpath.ClassPathFactory which is where the + * classpath is actually determined. + *

+ */ +public class RuntimePathBuilder { + + /* + * ============================================================== + * === List of modules and jars part of standard distribution === + * ============================================================== + * + * List of modules and to be included as part of a "standard" distribution which, at this point, + * contains the standard library and JFX. + */ + + /** + * The modules comprising the Java standard modules. + */ + protected static final String[] STANDARD_MODULES = { + "java.base.jmod", + "java.compiler.jmod", + "java.datatransfer.jmod", + "java.desktop.jmod", + "java.instrument.jmod", + "java.logging.jmod", + "java.management.jmod", + "java.management.rmi.jmod", + "java.naming.jmod", + "java.net.http.jmod", + "java.prefs.jmod", + "java.rmi.jmod", + "java.scripting.jmod", + "java.se.jmod", + "java.security.jgss.jmod", + "java.security.sasl.jmod", + "java.smartcardio.jmod", + "java.sql.jmod", + "java.sql.rowset.jmod", + "java.transaction.xa.jmod", + "java.xml.crypto.jmod", + "java.xml.jmod", + "jdk.accessibility.jmod", + "jdk.aot.jmod", + "jdk.attach.jmod", + "jdk.charsets.jmod", + "jdk.compiler.jmod", + "jdk.crypto.cryptoki.jmod", + "jdk.crypto.ec.jmod", + "jdk.dynalink.jmod", + "jdk.editpad.jmod", + "jdk.hotspot.agent.jmod", + "jdk.httpserver.jmod", + "jdk.internal.ed.jmod", + "jdk.internal.jvmstat.jmod", + "jdk.internal.le.jmod", + "jdk.internal.opt.jmod", + "jdk.internal.vm.ci.jmod", + "jdk.internal.vm.compiler.jmod", + "jdk.internal.vm.compiler.management.jmod", + "jdk.jartool.jmod", + "jdk.javadoc.jmod", + "jdk.jcmd.jmod", + "jdk.jconsole.jmod", + "jdk.jdeps.jmod", + "jdk.jdi.jmod", + "jdk.jdwp.agent.jmod", + "jdk.jfr.jmod", + "jdk.jlink.jmod", + "jdk.jshell.jmod", + "jdk.jsobject.jmod", + "jdk.jstatd.jmod", + "jdk.localedata.jmod", + "jdk.management.agent.jmod", + "jdk.management.jfr.jmod", + "jdk.management.jmod", + "jdk.naming.dns.jmod", + "jdk.naming.rmi.jmod", + "jdk.net.jmod", + "jdk.pack.jmod", + "jdk.rmic.jmod", + "jdk.scripting.nashorn.jmod", + "jdk.scripting.nashorn.shell.jmod", + "jdk.sctp.jmod", + "jdk.security.auth.jmod", + "jdk.security.jgss.jmod", + "jdk.unsupported.desktop.jmod", + "jdk.unsupported.jmod", + "jdk.xml.dom.jmod", + "jdk.zipfs.jmod" + }; + + /** + * The jars required for OpenJFX. + */ + protected static final String[] JAVA_FX_JARS = { + "javafx-swt.jar", + "javafx.base.jar", + "javafx.controls.jar", + "javafx.fxml.jar", + "javafx.graphics.jar", + "javafx.media.jar", + "javafx.swing.jar", + "javafx.web.jar" + }; + + /* + * ====================== + * === Path factories === + * ====================== + * + * There are multiple types of paths that are used in different contexts (sketch class path and + * import recommendations) and that are required to be re-calculated due to different events. + * + * The following collections determine which types of paths apply in each and are assigned in the + * constructor. Note that often factories are included in more than one of these collections + * and are cached independently as their values are invalidated at different events. + */ + + // Path caches that are invalidated by one or more events within processing. + private final List libraryDependentCaches; + private final List libraryImportsDependentCaches; + private final List codeFolderDependentCaches; + + // Path factories involved in determining sketch class path + private final List sketchClassPathStrategies; + + // Path factories involved in determining sketch search path (import recommendations) + private final List searchClassPathStrategies; + + // Inner class path factory + private final ClassPathFactory classPathFactory; + + /* + * ====================== + * === Public Methods === + * ====================== + */ + + /** + * Create a new runtime path builder with empty caches. + */ + public RuntimePathBuilder() { + + // Create the inner classpath factory. + classPathFactory = new ClassPathFactory(); + + // Build caches + CachedRuntimePathFactory javaRuntimePathFactory = new CachedRuntimePathFactory( + this::buildJavaRuntimePath + ); + CachedRuntimePathFactory javaFxRuntimePathFactory = new CachedRuntimePathFactory( + this::buildJavaFxRuntimePath + ); + CachedRuntimePathFactory modeSketchPathFactory = new CachedRuntimePathFactory( + this::buildModeSketchPath + ); + CachedRuntimePathFactory modeSearchPathFactory = new CachedRuntimePathFactory( + this::buildModeSearchPath + ); + CachedRuntimePathFactory librarySketchPathFactory = new CachedRuntimePathFactory( + this::buildLibrarySketchPath + ); + CachedRuntimePathFactory librarySearchPathFactory = new CachedRuntimePathFactory( + this::buildLibrarySearchPath + ); + CachedRuntimePathFactory coreLibraryPathFactory = new CachedRuntimePathFactory( + this::buildCoreLibraryPath + ); + CachedRuntimePathFactory codeFolderPathFactory = new CachedRuntimePathFactory( + this::buildCodeFolderPath + ); + + // Create collections for strategies + sketchClassPathStrategies = new ArrayList<>(); + searchClassPathStrategies = new ArrayList<>(); + libraryDependentCaches = new ArrayList<>(); + libraryImportsDependentCaches = new ArrayList<>(); + codeFolderDependentCaches = new ArrayList<>(); + + // Strategies required for the sketch class path at sketch execution + sketchClassPathStrategies.add(javaRuntimePathFactory); + sketchClassPathStrategies.add(javaFxRuntimePathFactory); + sketchClassPathStrategies.add(modeSketchPathFactory); + sketchClassPathStrategies.add(librarySketchPathFactory); + sketchClassPathStrategies.add(coreLibraryPathFactory); + sketchClassPathStrategies.add(codeFolderPathFactory); + + // Strategies required for import suggestions + searchClassPathStrategies.add(javaRuntimePathFactory); + searchClassPathStrategies.add(javaFxRuntimePathFactory); + searchClassPathStrategies.add(modeSearchPathFactory); + searchClassPathStrategies.add(librarySearchPathFactory); + searchClassPathStrategies.add(coreLibraryPathFactory); + searchClassPathStrategies.add(codeFolderPathFactory); + + // Assign strategies to collections for cache invalidation on library events. + libraryDependentCaches.add(coreLibraryPathFactory); + libraryImportsDependentCaches.add(librarySketchPathFactory); + libraryImportsDependentCaches.add(librarySearchPathFactory); + + // Assign strategies to collections for cache invalidation on code folder changes. + codeFolderDependentCaches.add(codeFolderPathFactory); + } + + /** + * Invalidate all of the runtime path caches associated with sketch libraries. + */ + public void markLibrariesChanged() { + invalidateAll(libraryDependentCaches); + } + + /** + * Invalidate all of the runtime path caches associated with sketch library imports. + */ + public void markLibraryImportsChanged() { + invalidateAll(libraryImportsDependentCaches); + } + + /** + * Invalidate all of the runtime path caches associated with the code folder having changed. + */ + public void markCodeFolderChanged() { + invalidateAll(codeFolderDependentCaches); + } + + /** + * Generate a classpath and inject it into a {PreprocessedSketch.Builder}. + * + * @param result The {PreprocessedSketch.Builder} into which the classpath should be inserted. + * @param mode The {JavaMode} for which the classpath should be generated. + */ + public void prepareClassPath(PreprocessedSketch.Builder result, JavaMode mode) { + List programImports = result.programImports; + Sketch sketch = result.sketch; + + prepareSketchClassPath(result, mode, programImports, sketch); + prepareSearchClassPath(result, mode, programImports, sketch); + } + + /** + * Invalidate all of the caches in a provided collection. + * + * @param caches The caches to invalidate so that, when their value is requested again, the value + * is generated again. + */ + private void invalidateAll(List caches) { + for (CachedRuntimePathFactory cache : caches) { + cache.invalidateCache(); + } + } + + /** + * Prepare the classpath required for the sketch's execution. + * + * @param result The PreprocessedSketch builder into which the classpath and class loader should + * be injected. + * @param mode The JavaMode for which a sketch classpath should be generated. + * @param programImports The imports listed by the sketch (user imports). + * @param sketch The sketch for which the classpath is being generated. + */ + private void prepareSketchClassPath(PreprocessedSketch.Builder result, JavaMode mode, + List programImports, Sketch sketch) { + + Stream sketchClassPath = sketchClassPathStrategies.stream() + .flatMap((x) -> x.buildClasspath(mode, programImports, sketch).stream()); + + String[] classPathArray = sketchClassPath.toArray(String[]::new); + URL[] urlArray = Arrays.stream(classPathArray) + .map(path -> { + try { + return Paths.get(path).toUri().toURL(); + } catch (MalformedURLException e) { + Messages.loge("malformed URL when preparing sketch classloader", e); + return null; + } + }) + .filter(Objects::nonNull) + .toArray(URL[]::new); + + result.classLoader = new URLClassLoader(urlArray, null); + result.classPath = classPathFactory.createFromPaths(classPathArray); + result.classPathArray = classPathArray; + } + + /** + * Prepare the classpath for searching in case of import suggestions. + * + * @param result The PreprocessedSketch builder into which the search classpath should be + * injected. + * @param mode The JavaMode for which a sketch classpath should be generated. + * @param programImports The imports listed by the sketch (user imports). + * @param sketch The sketch for which the classpath is being generated. + */ + private void prepareSearchClassPath(PreprocessedSketch.Builder result, JavaMode mode, + List programImports, Sketch sketch) { + + Stream searchClassPath = searchClassPathStrategies.stream() + .flatMap((x) -> x.buildClasspath(mode, programImports, sketch).stream()); + + result.searchClassPathArray = searchClassPath.toArray(String[]::new); + } + + /* + * =============================================================== + * ====== Methods for determining different kinds of paths. ====== + * =============================================================== + * + * Methods which help determine different paths for different types of classpath entries. Note + * that these are protected so that they can be tested. + */ + + /** + * Enumerate the modules as part of the java runtime. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildJavaRuntimePath(JavaMode mode, List imports, + Sketch sketch) { + + return Arrays.stream(STANDARD_MODULES) + .map(this::buildForModule) + .collect(Collectors.toList()); + } + + /** + * Enumerate the modules as part of the java runtime. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildJavaFxRuntimePath(JavaMode mode, List imports, + Sketch sketch) { + + return Arrays.stream(JAVA_FX_JARS) + .map(this::findFullyQualifiedJarName) + .collect(Collectors.toList()); + } + + /** + * Enumerate paths for resources like jars within the sketch code folder. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildCodeFolderPath(JavaMode mode, List imports, + Sketch sketch) { + + StringBuilder classPath = new StringBuilder(); + + // Code folder + if (sketch.hasCodeFolder()) { + File codeFolder = sketch.getCodeFolder(); + String codeFolderClassPath = Util.contentsToClassPath(codeFolder); + classPath.append(codeFolderClassPath); + } + + return sanitizeClassPath(classPath.toString()); + } + + /** + * Determine paths for libraries part of the processing mode (like {JavaMode}). + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildCoreLibraryPath(JavaMode mode, List imports, + Sketch sketch) { + + StringBuilder classPath = new StringBuilder(); + + for (Library lib : mode.coreLibraries) { + classPath.append(File.pathSeparator).append(lib.getClassPath()); + } + + return sanitizeClassPath(classPath.toString()); + } + + /** + * Generate classpath entries for third party libraries that are required for running the sketch. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildLibrarySketchPath(JavaMode mode, List imports, + Sketch sketch) { + + StringJoiner classPathBuilder = new StringJoiner(File.pathSeparator); + + imports.stream() + .map(ImportStatement::getPackageName) + .filter(pckg -> !isIgnorableForSketchPath(pckg)) + .map(pckg -> { + try { + return mode.getLibrary(pckg); + } catch (SketchException e) { + return null; + } + }) + .filter(Objects::nonNull) + .map(Library::getClassPath) + .forEach(classPathBuilder::add); + + return sanitizeClassPath(classPathBuilder.toString()); + } + + /** + * Generate classpath entries for third party libraries that are used when determining import + * recommendations. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildLibrarySearchPath(JavaMode mode, List imports, + Sketch sketch) { + + StringJoiner classPathBuilder = new StringJoiner(File.pathSeparator); + + for (Library lib : mode.contribLibraries) { + classPathBuilder.add(lib.getClassPath()); + } + + return sanitizeClassPath(classPathBuilder.toString()); + } + + /** + * Generate classpath entries for the processing mode (like {JavaMode}) used when making import + * recommendations. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildModeSearchPath(JavaMode mode, List imports, + Sketch sketch) { + + String searchClassPath = mode.getSearchPath(); + + if (searchClassPath != null) { + return sanitizeClassPath(searchClassPath); + } else { + return new ArrayList<>(); + } + } + + /** + * Generate classpath entries required by the processing mode like {JavaMode}. + * + * @param mode The mode engaged by the user like JavaMode. + * @param imports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + protected List buildModeSketchPath(JavaMode mode, List imports, + Sketch sketch) { + + Library coreLibrary = mode.getCoreLibrary(); + String coreClassPath = coreLibrary != null ? + coreLibrary.getClassPath() : mode.getSearchPath(); + if (coreClassPath != null) { + return sanitizeClassPath(coreClassPath); + } else { + return new ArrayList<>(); + } + } + + /* + * =============================================================== + * === Helper functions for the path generation methods above. === + * =============================================================== + * + * Note that these are made protected so that they can be tested. + */ + + /** + * Remove invalid entries in a classpath string. + * + * @param classPathString The classpath to clean. + * @return The cleaned classpath entries without invalid entries. + */ + protected List sanitizeClassPath(String classPathString) { + // Make sure class path does not contain empty string (home dir) + return Arrays.stream(classPathString.split(File.pathSeparator)) + .filter(p -> p != null && !p.trim().isEmpty()) + .distinct() + .collect(Collectors.toList()); + } + + /** + * Determine if a package is ignorable because it is standard. + * + * Determine if a package is ignorable on the sketch path because it is standard. This is + * different than being ignorable in imports recommendations. + * + * @param packageName The name of the package to evaluate. + * @return True if the package is part of standard Java (like java.lang.*). False otherwise. + */ + protected boolean isIgnorableForSketchPath(String packageName) { + return (packageName.startsWith("java.") || packageName.startsWith("javax.")); + } + + /** + * Find a fully qualified jar name. + * + * @param jarName The jar name like "javafx.base.jar" for which a fully qualified entry should be + * created. + * @return The fully qualified classpath entry like ".../Processing.app/Contents/PlugIns/ + * adoptopenjdk-11.0.1.jdk/Contents/Home/lib/javafx.base.jar" + */ + protected String findFullyQualifiedJarName(String jarName) { + StringJoiner joiner = new StringJoiner(File.separator); + joiner.add(System.getProperty("java.home")); + joiner.add("lib"); + joiner.add(jarName); + + return joiner.toString(); + } + + /** + * Build a classpath entry for a module. + * + * @param moduleName The name of the module like "java.base.jmod". + * @return The fully qualified classpath entry like ".../Processing.app/Contents/PlugIns/ + * adoptopenjdk-11.0.1.jdk/Contents/Home/jmods/java.base.jmod" + */ + protected String buildForModule(String moduleName) { + StringJoiner jmodPathJoiner = new StringJoiner(File.separator); + jmodPathJoiner.add(System.getProperty("java.home")); + jmodPathJoiner.add("jmods"); + jmodPathJoiner.add(moduleName); + return jmodPathJoiner.toString(); + } + + /* + * ============================================ + * === Interface definitions and utilities. === + * ============================================ + * + * Note that these are protected so that they can be tested. The interface below defines a + * strategy for determining path elements. An optional caching object which allows for path + * invalidatino is also defined below. + */ + + /** + * Strategy which generates part of the classpath and/or module path. + * + *

+ * Strategy for factories each of which generate part of the classpath and/or module path required + * by a sketch through user supplied requirements, mode (as in JavaMode) requirements, or + * transitive requirements imposed by third party libraries. + *

+ */ + protected interface RuntimePathFactoryStrategy { + + /** + * Create classpath and/or module path entries. + * + * @param mode The mode engaged by the user like JavaMode. + * @param programImports The program imports imposed by the user within their sketch. + * @param sketch The sketch provided by the user. + * @return List of classpath and/or module path entries. + */ + List buildClasspath(JavaMode mode, List programImports, Sketch sketch); + + } + + /** + * Runtime path factory which caches the results of another runtime path factory. + * + *

+ * Runtime path factory which decorates another {RuntimePathFactoryStrategy} that caches the + * results of another runtime path factory. This is a lazy cached getter so the value will not be + * resolved until it is requested. + *

+ */ + protected static class CachedRuntimePathFactory implements RuntimePathFactoryStrategy { + + private AtomicReference> cachedResult; + private RuntimePathFactoryStrategy innerStrategy; + + /** + * Create a new cache around {RuntimePathFactoryStrategy}. + * + * @param newInnerStrategy The strategy to cache. + */ + public CachedRuntimePathFactory(RuntimePathFactoryStrategy newInnerStrategy) { + cachedResult = new AtomicReference<>(null); + innerStrategy = newInnerStrategy; + } + + /** + * Invalidate the cached path so that, when requested next time, it will be rebuilt from + * scratch. + */ + public void invalidateCache() { + cachedResult.set(null); + } + + /** + * Return the cached classpath or, if not cached, build a classpath using the inner strategy. + * + *

+ * Return the cached classpath or, if not cached, build a classpath using the inner strategy. + * Note that this getter will not check to see if mode, imports, or sketch have changed. If a + * cached value is available, it will be returned without examining the identity of the + * parameters. + *

+ * + * @param mode The {JavaMode} for which the classpath should be built. + * @param imports The sketch (user) imports. + * @param sketch The sketch for which a classpath is to be returned. + * @return Newly generated classpath. + */ + @Override + public List buildClasspath(JavaMode mode, List imports, + Sketch sketch) { + + return cachedResult.updateAndGet((cachedValue) -> + cachedValue == null ? innerStrategy.buildClasspath(mode, imports, sketch) : cachedValue + ); + } + + } + +} diff --git a/java/test/processing/mode/java/pdex/util/RuntimePathBuilderTest.java b/java/test/processing/mode/java/pdex/util/RuntimePathBuilderTest.java new file mode 100644 index 000000000..19fc76b80 --- /dev/null +++ b/java/test/processing/mode/java/pdex/util/RuntimePathBuilderTest.java @@ -0,0 +1,156 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* +Part of the Processing project - http://processing.org +Copyright (c) 2019 The Processing Foundation + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +package processing.mode.java.pdex.util; + +import org.junit.Before; +import org.junit.Test; +import processing.app.Sketch; +import processing.mode.java.JavaMode; +import processing.mode.java.pdex.ImportStatement; +import processing.mode.java.pdex.PreprocessedSketch; +import processing.mode.java.pdex.util.RuntimePathFactoryTestUtil; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; + +import static org.junit.Assert.*; + + +public class RuntimePathBuilderTest { + + private RuntimePathBuilder builder; + private JavaMode testMode; + private List testImports; + private Sketch testSketch; + private PreprocessedSketch.Builder result; + + @Before + public void setUp() throws Exception { + builder = new RuntimePathBuilder(); + testMode = RuntimePathFactoryTestUtil.createTestJavaMode(); + testImports = RuntimePathFactoryTestUtil.createTestImports(); + testSketch = RuntimePathFactoryTestUtil.createTestSketch(); + + result = new PreprocessedSketch.Builder(); + result.programImports.addAll(testImports); + result.sketch = testSketch; + + builder.prepareClassPath(result, testMode); + } + + @Test + public void testClassPathLoader() { + assertNotNull(result.classLoader); + } + + @Test + public void testClassPathObj() { + assertNotNull(result.classPath); + } + + @Test + public void testSketchClassPathStrategiesJava() { + checkPresent(result.classPathArray, "java.base.jmod"); + } + + @Test + public void testSketchClassPathStrategiesLibrary() { + checkPresent(result.classPathArray, "library3"); + } + + @Test + public void testSketchClassPathStrategiesCore() { + checkPresent(result.classPathArray, "library3"); + } + + @Test + public void testSketchClassPathStrategiesMode() { + checkPresent(result.classPathArray, "library6"); + } + + @Test + public void testSketchClassPathStrategiesCodeFolder() { + checkPresent(result.classPathArray, "file1.jar"); + } + + @Test + public void testSearchClassPathStrategiesCodeJava() { + checkPresent(result.searchClassPathArray, "java.base.jmod"); + } + + @Test + public void testSearchClassPathStrategiesCodeMode() { + checkPresent(result.classPathArray, "library6"); + } + + @Test + public void testSearchClassPathStrategiesCodeLibrary() { + checkPresent(result.classPathArray, "library3"); + } + + @Test + public void testSearchClassPathStrategiesCodeCore() { + checkPresent(result.classPathArray, "library1"); + } + + @Test + public void testSearchClassPathStrategiesCodeCodeFolder() { + checkPresent(result.classPathArray, "file3.zip"); + } + + private void checkPresent(String[] classPathArray, String target) { + long count = Arrays.stream(classPathArray) + .filter((x) -> x.contains(target)) + .count(); + + assertTrue(count > 0); + } + + @Test + public void sanitizeClassPath() { + StringJoiner testStrJoiner = new StringJoiner(File.pathSeparator); + testStrJoiner.add("test1"); + testStrJoiner.add(""); + testStrJoiner.add("test2"); + + List classPath = builder.sanitizeClassPath(testStrJoiner.toString()); + assertEquals(2, classPath.size()); + assertEquals("test1", classPath.get(0)); + assertEquals("test2", classPath.get(1)); + } + + @Test + public void sanitizeClassPathNoDuplicate() { + StringJoiner testStrJoiner = new StringJoiner(File.pathSeparator); + testStrJoiner.add("test1"); + testStrJoiner.add(""); + testStrJoiner.add("test2"); + testStrJoiner.add("test2"); + + List classPath = builder.sanitizeClassPath(testStrJoiner.toString()); + assertEquals(2, classPath.size()); + assertEquals("test1", classPath.get(0)); + assertEquals("test2", classPath.get(1)); + } + +}