mirror of
https://github.com/apple/pkl.git
synced 2026-03-19 07:43:59 +01:00
Use java.net.http.HttpClient instead of java.net.Http(s)URLConnection (#217)
Moving to java.net.http.HttpClient brings many benefits, including HTTP/2 support and the ability to make asynchronous requests. Major additions and changes: - Introduce a lightweight org.pkl.core.http.HttpClient API. This keeps some flexibility and allows to enforce behavior such as setting the User-Agent header. - Provide an implementation that delegates to java.net.http.HttpClient. - Use HttpClient for all HTTP(s) requests across the codebase. This required adding an HttpClient parameter to constructors and factory methods of multiple classes, some of which are public APIs. - Manage CA certificates per HTTP client instead of per JVM. This makes it unnecessary to set JVM-wide system/security properties and default SSLSocketFactory's. - Add executor v2 options to the executor SPI - Add pkl-certs as a new artifact, and remove certs from pkl-commons-cli artifact Each HTTP client maintains its own connection pool and SSLContext. For efficiency reasons, It's best to reuse clients whenever feasible. To avoid memory leaks, clients are not stored in static fields. HTTP clients are expensive to create. For this reason, EvaluatorBuilder defaults to a "lazy" client that creates the underlying java.net.http.HttpClient on the first send (which may never happen).
This commit is contained in:
@@ -27,7 +27,6 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpi;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -40,12 +39,17 @@ final class EmbeddedExecutor implements Executor {
|
||||
private final List<PklDistribution> pklDistributions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if a Jar file cannot be found or is not a valid PklPkl
|
||||
* @throws IllegalArgumentException if a Jar file cannot be found or is not a valid Pkl
|
||||
* distribution
|
||||
*/
|
||||
public EmbeddedExecutor(List<Path> pklFatJars) {
|
||||
this(pklFatJars, Executor.class.getClassLoader());
|
||||
}
|
||||
|
||||
// for testing only
|
||||
EmbeddedExecutor(List<Path> pklFatJars, ClassLoader pklExecutorClassLoader) {
|
||||
for (var jarFile : pklFatJars) {
|
||||
pklDistributions.add(new PklDistribution(jarFile));
|
||||
pklDistributions.add(new PklDistribution(jarFile, pklExecutorClassLoader));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +76,7 @@ final class EmbeddedExecutor implements Executor {
|
||||
// (but not any modules imported by it) and only requires parsing (but not evaluating) the
|
||||
// module.
|
||||
requestedVersion = detectRequestedPklVersion(modulePath, options);
|
||||
//noinspection resource
|
||||
distribution = findCompatibleDistribution(modulePath, requestedVersion, options);
|
||||
output = distribution.evaluatePath(modulePath, options);
|
||||
} catch (RuntimeException e) {
|
||||
@@ -163,22 +168,23 @@ final class EmbeddedExecutor implements Executor {
|
||||
}
|
||||
|
||||
private static final class PklDistribution implements AutoCloseable {
|
||||
final URLClassLoader classLoader;
|
||||
final ExecutorSpi executorSpi;
|
||||
final URLClassLoader pklDistributionClassLoader;
|
||||
final /* @Nullable */ ExecutorSpi executorSpi;
|
||||
final Version version;
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if the Jar file does not exist or is not a valid Pkl
|
||||
* distribution
|
||||
*/
|
||||
PklDistribution(Path pklFatJar) {
|
||||
PklDistribution(Path pklFatJar, ClassLoader pklExecutorClassLoader) {
|
||||
if (!Files.isRegularFile(pklFatJar)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Invalid Pkl distribution: Cannot find Jar file `%s`.", pklFatJar));
|
||||
}
|
||||
|
||||
classLoader = new PklDistributionClassLoader(pklFatJar);
|
||||
var serviceLoader = ServiceLoader.load(ExecutorSpi.class, classLoader);
|
||||
pklDistributionClassLoader =
|
||||
new PklDistributionClassLoader(pklFatJar, pklExecutorClassLoader);
|
||||
var serviceLoader = ServiceLoader.load(ExecutorSpi.class, pklDistributionClassLoader);
|
||||
|
||||
try {
|
||||
executorSpi = serviceLoader.iterator().next();
|
||||
@@ -208,9 +214,9 @@ final class EmbeddedExecutor implements Executor {
|
||||
var currentThread = Thread.currentThread();
|
||||
var prevContextClassLoader = currentThread.getContextClassLoader();
|
||||
// Truffle loads stuff from context class loader, so set it to our class loader
|
||||
currentThread.setContextClassLoader(classLoader);
|
||||
currentThread.setContextClassLoader(pklDistributionClassLoader);
|
||||
try {
|
||||
return executorSpi.evaluatePath(modulePath, toEvaluatorOptions(options));
|
||||
return executorSpi.evaluatePath(modulePath, options.toSpiOptions());
|
||||
} catch (ExecutorSpiException e) {
|
||||
throw new ExecutorException(e.getMessage(), e.getCause());
|
||||
} finally {
|
||||
@@ -220,30 +226,21 @@ final class EmbeddedExecutor implements Executor {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
classLoader.close();
|
||||
}
|
||||
|
||||
ExecutorSpiOptions toEvaluatorOptions(ExecutorOptions options) {
|
||||
return new ExecutorSpiOptions(
|
||||
options.getAllowedModules(),
|
||||
options.getAllowedResources(),
|
||||
options.getEnvironmentVariables(),
|
||||
options.getExternalProperties(),
|
||||
options.getModulePath(),
|
||||
options.getRootDir(),
|
||||
options.getTimeout(),
|
||||
options.getOutputFormat(),
|
||||
options.getModuleCacheDir(),
|
||||
options.getProjectDir());
|
||||
pklDistributionClassLoader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PklDistributionClassLoader extends URLClassLoader {
|
||||
final ClassLoader spiClassLoader = ExecutorSpi.class.getClassLoader();
|
||||
final ClassLoader pklExecutorClassLoader;
|
||||
|
||||
PklDistributionClassLoader(Path pklFatJar) {
|
||||
static {
|
||||
registerAsParallelCapable();
|
||||
}
|
||||
|
||||
PklDistributionClassLoader(Path pklFatJar, ClassLoader pklExecutorClassLoader) {
|
||||
// pass `null` to make bootstrap class loader the effective parent
|
||||
super(toUrls(pklFatJar), null);
|
||||
this.pklExecutorClassLoader = pklExecutorClassLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -253,7 +250,15 @@ final class EmbeddedExecutor implements Executor {
|
||||
|
||||
if (clazz == null) {
|
||||
if (name.startsWith("org.pkl.executor.spi.")) {
|
||||
clazz = spiClassLoader.loadClass(name);
|
||||
try {
|
||||
// give pkl-executor a chance to load the SPI clasa
|
||||
clazz = pklExecutorClassLoader.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// The SPI class exists in this distribution but not in pkl-executor,
|
||||
// so load it from the distribution.
|
||||
// This can happen if the pkl-executor version is lower than the distribution version.
|
||||
clazz = findClass(name);
|
||||
}
|
||||
} else if (name.startsWith("java.")
|
||||
|| name.startsWith("jdk.")
|
||||
|| name.startsWith("sun.")
|
||||
@@ -282,18 +287,14 @@ final class EmbeddedExecutor implements Executor {
|
||||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
// try bootstrap class loader first
|
||||
// once we move to JDK 9+, should use `getPlatformClassLoader().getResource()` instead of
|
||||
// `super.getResource()`
|
||||
var resource = super.getResource(name);
|
||||
var resource = getPlatformClassLoader().getResource(name);
|
||||
return resource != null ? resource : findResource(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
// once we move to JDK 9+, should use `getPlatformClassLoader().getResources()` instead of
|
||||
// `super.getResources()`
|
||||
return ConcatenatedEnumeration.create(super.getResources(name), findResources(name));
|
||||
return ConcatenatedEnumeration.create(
|
||||
getPlatformClassLoader().getResources(name), findResources(name));
|
||||
}
|
||||
|
||||
static URL[] toUrls(Path pklFatJar) {
|
||||
|
||||
@@ -20,27 +20,33 @@ import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||
|
||||
/** Options for {@link Executor#evaluatePath}. */
|
||||
public final class ExecutorOptions {
|
||||
private final List<String> allowedModules;
|
||||
/**
|
||||
* Options for {@link Executor#evaluatePath}.
|
||||
*
|
||||
* <p>Note that subclasses of {@code ExecutorOptions} offer additional options.
|
||||
*/
|
||||
public class ExecutorOptions {
|
||||
protected final List<String> allowedModules;
|
||||
|
||||
private final List<String> allowedResources;
|
||||
protected final List<String> allowedResources;
|
||||
|
||||
private final Map<String, String> environmentVariables;
|
||||
protected final Map<String, String> environmentVariables;
|
||||
|
||||
private final Map<String, String> externalProperties;
|
||||
protected final Map<String, String> externalProperties;
|
||||
|
||||
private final List<Path> modulePath;
|
||||
protected final List<Path> modulePath;
|
||||
|
||||
private final /* @Nullable */ Path rootDir;
|
||||
protected final /* @Nullable */ Path rootDir;
|
||||
|
||||
private final /* @Nullable */ Duration timeout;
|
||||
protected final /* @Nullable */ Duration timeout;
|
||||
|
||||
private final /* @Nullable */ String outputFormat;
|
||||
protected final /* @Nullable */ String outputFormat;
|
||||
|
||||
private final /* @Nullable */ Path moduleCacheDir;
|
||||
private final /* @Nullable */ Path projectDir;
|
||||
protected final /* @Nullable */ Path moduleCacheDir;
|
||||
|
||||
protected final /* @Nullable */ Path projectDir;
|
||||
|
||||
/** Returns the module cache dir that the CLI uses by default. */
|
||||
public static Path defaultModuleCacheDir() {
|
||||
@@ -148,7 +154,7 @@ public final class ExecutorOptions {
|
||||
@Override
|
||||
public boolean equals(/* @Nullable */ Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof ExecutorOptions)) return false;
|
||||
if (obj.getClass() != ExecutorOptions.class) return false;
|
||||
|
||||
var other = (ExecutorOptions) obj;
|
||||
return allowedModules.equals(other.allowedModules)
|
||||
@@ -203,4 +209,18 @@ public final class ExecutorOptions {
|
||||
+ projectDir
|
||||
+ '}';
|
||||
}
|
||||
|
||||
ExecutorSpiOptions toSpiOptions() {
|
||||
return new ExecutorSpiOptions(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.executor;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
|
||||
|
||||
/**
|
||||
* Options for {@link Executor#evaluatePath}.
|
||||
*
|
||||
* <p>This class offers additional options not available in {@code ExecutorOptions}.
|
||||
*/
|
||||
public class ExecutorOptions2 extends ExecutorOptions {
|
||||
protected final List<Path> certificateFiles;
|
||||
protected final List<URI> certificateUris;
|
||||
|
||||
/**
|
||||
* Constructs an options object.
|
||||
*
|
||||
* @param allowedModules API equivalent of the {@code --allowed-modules} CLI option
|
||||
* @param allowedResources API equivalent of the {@code --allowed-resources} CLI option
|
||||
* @param environmentVariables API equivalent of the repeatable {@code --env-var} CLI option
|
||||
* @param externalProperties API equivalent of the repeatable {@code --property} CLI option
|
||||
* @param modulePath API equivalent of the {@code --module-path} CLI option
|
||||
* @param rootDir API equivalent of the {@code --root-dir} CLI option
|
||||
* @param timeout API equivalent of the {@code --timeout} CLI option
|
||||
* @param outputFormat API equivalent of the {@code --format} CLI option
|
||||
* @param moduleCacheDir API equivalent of the {@code --cache-dir} CLI option. Passing {@link
|
||||
* #defaultModuleCacheDir()} is equivalent to omitting {@code --cache-dir}. Passing {@code
|
||||
* null} is equivalent to {@code --no-cache}.
|
||||
* @param projectDir API equivalent of the {@code --project-dir} CLI option.
|
||||
* @param certificateFiles API equivalent of the {@code --ca-certificates} CLI option
|
||||
* @param certificateUris API equivalent of the {@code --ca-certificates} CLI option
|
||||
*/
|
||||
public ExecutorOptions2(
|
||||
List<String> allowedModules,
|
||||
List<String> allowedResources,
|
||||
Map<String, String> environmentVariables,
|
||||
Map<String, String> externalProperties,
|
||||
List<Path> modulePath,
|
||||
/* @Nullable */ Path rootDir,
|
||||
/* @Nullable */ Duration timeout,
|
||||
/* @Nullable */ String outputFormat,
|
||||
/* @Nullable */ Path moduleCacheDir,
|
||||
/* @Nullable */ Path projectDir,
|
||||
List<Path> certificateFiles,
|
||||
List<URI> certificateUris) {
|
||||
|
||||
super(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir);
|
||||
this.certificateFiles = certificateFiles;
|
||||
this.certificateUris = certificateUris;
|
||||
}
|
||||
|
||||
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
||||
public List<Path> getCertificateFiles() {
|
||||
return certificateFiles;
|
||||
}
|
||||
|
||||
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
||||
public List<URI> getCertificateUris() {
|
||||
return certificateUris;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(/* @Nullable */ Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj.getClass() != ExecutorOptions2.class) return false;
|
||||
|
||||
var other = (ExecutorOptions2) obj;
|
||||
return allowedModules.equals(other.allowedModules)
|
||||
&& allowedResources.equals(other.allowedResources)
|
||||
&& environmentVariables.equals(other.environmentVariables)
|
||||
&& externalProperties.equals(other.externalProperties)
|
||||
&& modulePath.equals(other.modulePath)
|
||||
&& Objects.equals(rootDir, other.rootDir)
|
||||
&& Objects.equals(timeout, other.timeout)
|
||||
&& Objects.equals(outputFormat, other.outputFormat)
|
||||
&& Objects.equals(moduleCacheDir, other.moduleCacheDir)
|
||||
&& Objects.equals(projectDir, other.projectDir)
|
||||
&& Objects.equals(certificateFiles, other.certificateFiles)
|
||||
&& Objects.equals(certificateUris, other.certificateUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir,
|
||||
certificateFiles,
|
||||
certificateUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExecutorOptions2{"
|
||||
+ "allowedModules="
|
||||
+ allowedModules
|
||||
+ ", allowedResources="
|
||||
+ allowedResources
|
||||
+ ", environmentVariables="
|
||||
+ environmentVariables
|
||||
+ ", externalProperties="
|
||||
+ externalProperties
|
||||
+ ", modulePath="
|
||||
+ modulePath
|
||||
+ ", rootDir="
|
||||
+ rootDir
|
||||
+ ", timeout="
|
||||
+ timeout
|
||||
+ ", outputFormat="
|
||||
+ outputFormat
|
||||
+ ", cacheDir="
|
||||
+ moduleCacheDir
|
||||
+ ", projectDir="
|
||||
+ projectDir
|
||||
+ ", certificateFiles="
|
||||
+ certificateFiles
|
||||
+ ", certificateUris="
|
||||
+ certificateUris
|
||||
+ '}';
|
||||
}
|
||||
|
||||
ExecutorSpiOptions2 toSpiOptions() {
|
||||
return new ExecutorSpiOptions2(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir,
|
||||
certificateFiles,
|
||||
certificateUris);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.executor.spi.v1;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
||||
private final List<Path> certificateFiles;
|
||||
|
||||
private final List<URI> certificateUris;
|
||||
|
||||
public ExecutorSpiOptions2(
|
||||
List<String> allowedModules,
|
||||
List<String> allowedResources,
|
||||
Map<String, String> environmentVariables,
|
||||
Map<String, String> externalProperties,
|
||||
List<Path> modulePath,
|
||||
Path rootDir,
|
||||
Duration timeout,
|
||||
String outputFormat,
|
||||
Path moduleCacheDir,
|
||||
Path projectDir,
|
||||
List<Path> certificateFiles,
|
||||
List<URI> certificateUris) {
|
||||
super(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir);
|
||||
this.certificateFiles = certificateFiles;
|
||||
this.certificateUris = certificateUris;
|
||||
}
|
||||
|
||||
public List<Path> getCertificateFiles() {
|
||||
return certificateFiles;
|
||||
}
|
||||
|
||||
public List<URI> getCertificateUris() {
|
||||
return certificateUris;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,159 @@
|
||||
package org.pkl.executor
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.commons.test.FilteringClassLoader
|
||||
import org.pkl.commons.test.PackageServer
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.commons.walk
|
||||
import org.pkl.core.runtime.CertificateUtils
|
||||
import org.pkl.core.Release
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.exists
|
||||
|
||||
class EmbeddedExecutorTest {
|
||||
private val pklDistribution by lazy {
|
||||
val libsDir = FileTestUtils.rootProjectDir.resolve("pkl-config-java/build/libs")
|
||||
if (!Files.isDirectory(libsDir)) {
|
||||
throw AssertionError(
|
||||
"JAR `pkl-config-java-all` does not exist. Run `./gradlew :pkl-config-java:build` to create it."
|
||||
/**
|
||||
* A combination of ExecutorOptions version, pkl-executor version,
|
||||
* and Pkl distribution version that parameterized tests should be run against.
|
||||
*/
|
||||
data class ExecutionContext(
|
||||
val executor: Executor,
|
||||
val options: (ExecutorOptions) -> ExecutorOptions,
|
||||
val name: String
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val allExecutionContexts: List<ExecutionContext> by lazy {
|
||||
listOf(
|
||||
ExecutionContext(executor1_1.value, ::convertToOptions1, "Options1, Executor1, Distribution1"),
|
||||
|
||||
// This context has a pkl-executor version that is lower than the distribution version.
|
||||
// It can be enabled once there is a distribution that includes pkl-executor.
|
||||
//ExecutionContext(executor1_2.value, ::convertToOptions1, "Options1, Executor1, Distribution2"),
|
||||
|
||||
ExecutionContext(executor2_1.value, ::convertToOptions1, "Options1, Executor2, Distribution1"),
|
||||
ExecutionContext(executor2_1.value, ::convertToOptions2, "Options2, Executor2, Distribution1"),
|
||||
|
||||
ExecutionContext(executor2_2.value, ::convertToOptions1, "Options1, Executor2, Distribution2"),
|
||||
ExecutionContext(executor2_2.value, ::convertToOptions2, "Options2, Executor2, Distribution2")
|
||||
)
|
||||
}
|
||||
libsDir.walk()
|
||||
.filter { path ->
|
||||
path.toString().let {
|
||||
it.contains("-all") &&
|
||||
it.endsWith(".jar") &&
|
||||
!it.contains("-sources") &&
|
||||
!it.contains("-javadoc")
|
||||
|
||||
private val currentExecutor: Executor by lazy { executor2_2.value }
|
||||
|
||||
// A pkl-executor library that supports ExecutorSpiOptions up to v1
|
||||
// and a Pkl distribution that supports ExecutorSpiOptions up to v1.
|
||||
private val executor1_1: Lazy<Executor> = lazy {
|
||||
EmbeddedExecutor(listOf(pklDistribution1), pklExecutorClassLoader1)
|
||||
}
|
||||
|
||||
// A pkl-executor library that supports ExecutorSpiOptions up to v1
|
||||
// and a Pkl distribution that supports ExecutorSpiOptions up to v2.
|
||||
private val executor1_2: Lazy<Executor> = lazy {
|
||||
EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader1)
|
||||
}
|
||||
|
||||
// A pkl-executor library that supports ExecutorSpiOptions up to v2
|
||||
// and a Pkl distribution that supports ExecutorSpiOptions up to v1.
|
||||
private val executor2_1: Lazy<Executor> = lazy {
|
||||
EmbeddedExecutor(listOf(pklDistribution1), pklExecutorClassLoader2)
|
||||
}
|
||||
|
||||
// A pkl-executor library that supports ExecutorSpiOptions up to v2
|
||||
// and a Pkl distribution that supports ExecutorSpiOptions up to v.
|
||||
private val executor2_2: Lazy<Executor> = lazy {
|
||||
EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader2)
|
||||
}
|
||||
|
||||
private val allExecutors by lazy {
|
||||
listOf(executor1_1, executor1_2, executor2_1, executor2_2)
|
||||
}
|
||||
|
||||
// a pkl-executor class loader that supports ExecutorSpiOptions up to v1
|
||||
private val pklExecutorClassLoader1: ClassLoader by lazy {
|
||||
FilteringClassLoader(pklExecutorClassLoader2) { className ->
|
||||
!className.endsWith("ExecutorSpiOptions2")
|
||||
}
|
||||
}
|
||||
|
||||
// a pkl-executor class loader that supports ExecutorSpiOptions up to v2
|
||||
private val pklExecutorClassLoader2: ClassLoader by lazy {
|
||||
EmbeddedExecutor::class.java.classLoader
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@JvmStatic
|
||||
fun afterAll() {
|
||||
for (executor in allExecutors) {
|
||||
if (executor.isInitialized()) executor.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// a Pkl distribution that supports ExecutorSpiOptions up to v1
|
||||
private val pklDistribution1: Path by lazy {
|
||||
val path = System.getProperty("pklDistribution025")?.toPath() ?:
|
||||
// can get rid of this path by switching to IntelliJ's Gradle test runner
|
||||
System.getProperty("user.home").toPath()
|
||||
.resolve(".gradle/caches/modules-2/files-2.1/org.pkl-lang/pkl-config-java-all/" +
|
||||
"0.25.0/e9451dda554f1659e49ff5bdd30accd26be7bf0f/pkl-config-java-all-0.25.0.jar")
|
||||
path.apply {
|
||||
if (!exists()) throw AssertionError("Missing test fixture. " +
|
||||
"To fix this problem, run `./gradlew :pkl-executor:prepareTest`.")
|
||||
}
|
||||
}
|
||||
|
||||
// a Pkl distribution that supports ExecutorSpiOptions up to v2
|
||||
private val pklDistribution2: Path by lazy {
|
||||
val path = System.getProperty("pklDistributionCurrent")?.toPath() ?:
|
||||
// can get rid of this path by switching to IntelliJ's Gradle test runner
|
||||
FileTestUtils.rootProjectDir
|
||||
.resolve("pkl-config-java/build/libs/pkl-config-java-all-" +
|
||||
"${Release.current().version().withBuild(null).toString().replaceFirst("dev", "SNAPSHOT")}.jar")
|
||||
path.apply {
|
||||
if (!exists()) throw AssertionError("Missing test fixture. " +
|
||||
"To fix this problem, run `./gradlew :pkl-executor:prepareTest`.")
|
||||
}
|
||||
.findFirst()
|
||||
.orElseThrow {
|
||||
AssertionError(
|
||||
"JAR `pkl-config-java-all` does not exist. Run `./gradlew :pkl-config-java:build` to create it."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToOptions2(options: ExecutorOptions): ExecutorOptions2 =
|
||||
if (options is ExecutorOptions2) options else ExecutorOptions2(
|
||||
options.allowedModules,
|
||||
options.allowedResources,
|
||||
options.environmentVariables,
|
||||
options.externalProperties,
|
||||
options.modulePath,
|
||||
options.rootDir,
|
||||
options.timeout,
|
||||
options.outputFormat,
|
||||
options.moduleCacheDir,
|
||||
options.projectDir,
|
||||
listOf(),
|
||||
listOf()
|
||||
)
|
||||
|
||||
private fun convertToOptions1(options: ExecutorOptions): ExecutorOptions =
|
||||
if (options.javaClass == ExecutorOptions::class.java) options else ExecutorOptions(
|
||||
options.allowedModules,
|
||||
options.allowedResources,
|
||||
options.environmentVariables,
|
||||
options.externalProperties,
|
||||
options.modulePath,
|
||||
options.rootDir,
|
||||
options.timeout,
|
||||
options.outputFormat,
|
||||
options.moduleCacheDir,
|
||||
options.projectDir
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -121,8 +238,9 @@ class EmbeddedExecutorTest {
|
||||
.contains("pkl.jar")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a module that is missing a ModuleInfo annotation`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `evaluate a module that is missing a ModuleInfo annotation`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -132,12 +250,10 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val e = assertThrows<ExecutorException> {
|
||||
executor.use {
|
||||
it.evaluatePath(
|
||||
context.executor.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:"),
|
||||
listOf("prop:"),
|
||||
mapOf(),
|
||||
@@ -147,18 +263,20 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
listOf(),
|
||||
listOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
assertThat(e.message)
|
||||
.contains("Pkl module `test.pkl` does not state which Pkl version it requires.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a module that requests an incompatible Pkl version`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `evaluate a module that requests an incompatible Pkl version`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -169,12 +287,10 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val e = assertThrows<ExecutorException> {
|
||||
executor.use {
|
||||
it.evaluatePath(
|
||||
context.executor.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:"),
|
||||
listOf("prop:"),
|
||||
mapOf(),
|
||||
@@ -184,18 +300,20 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
null,
|
||||
listOf(),
|
||||
listOf()
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(e.message)
|
||||
.contains("Pkl version `99.99.99` requested by module `test.pkl` is not supported.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a module that reads environment variables and external properties`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `evaluate a module that reads environment variables and external properties`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -207,11 +325,9 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val result = executor.use {
|
||||
it.evaluatePath(
|
||||
val result = context.executor.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:"),
|
||||
// should `prop:pkl.outputFormat` be allowed automatically?
|
||||
listOf("prop:", "env:"),
|
||||
@@ -222,10 +338,11 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
null,
|
||||
listOf(),
|
||||
listOf()
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(result.trim()).isEqualTo(
|
||||
"""
|
||||
@@ -235,8 +352,9 @@ class EmbeddedExecutorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a module that depends on another module`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `evaluate a module that depends on another module`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -260,11 +378,9 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val result = executor.use {
|
||||
it.evaluatePath(
|
||||
val result = context.executor.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:"),
|
||||
listOf("prop:"),
|
||||
mapOf(),
|
||||
@@ -274,10 +390,12 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
listOf(),
|
||||
listOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
assertThat(result.trim()).isEqualTo(
|
||||
"""
|
||||
@@ -288,8 +406,9 @@ class EmbeddedExecutorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a module whose evaluation fails`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `evaluate a module whose evaluation fails`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -300,12 +419,10 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val e = assertThrows<ExecutorException> {
|
||||
executor.use {
|
||||
it.evaluatePath(
|
||||
context.executor.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:"),
|
||||
listOf("prop:"),
|
||||
mapOf(),
|
||||
@@ -315,10 +432,11 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
listOf(),
|
||||
listOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
assertThat(e.message)
|
||||
@@ -328,8 +446,9 @@ class EmbeddedExecutorTest {
|
||||
.doesNotContain(tempDir.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `time out a module`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `time out a module`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -342,12 +461,10 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val e = assertThrows<ExecutorException> {
|
||||
executor.use {
|
||||
it.evaluatePath(
|
||||
context.executor.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:"),
|
||||
listOf("prop:"),
|
||||
mapOf(),
|
||||
@@ -357,59 +474,20 @@ class EmbeddedExecutorTest {
|
||||
Duration.ofSeconds(1),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
null,
|
||||
listOf(),
|
||||
listOf()
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(e.message)
|
||||
.contains("Evaluation timed out after 1 second(s).")
|
||||
}
|
||||
|
||||
// Only packages are cached.
|
||||
// Because this test doesn't import a package, it doesn't really test
|
||||
// that the `moduleCacheDir` option takes effect.
|
||||
@Test
|
||||
fun `evaluate a module with enabled module cache`(@TempDir tempDir: Path) {
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@ModuleInfo { minPklVersion = "0.16.0" }
|
||||
module test
|
||||
|
||||
x = 42
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val result = executor.use {
|
||||
it.evaluatePath(
|
||||
pklFile,
|
||||
ExecutorOptions(
|
||||
listOf("file:"),
|
||||
listOf("prop:"),
|
||||
mapOf(),
|
||||
mapOf(),
|
||||
listOf(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
ExecutorOptions.defaultModuleCacheDir(),
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(result.trim()).isEqualTo(
|
||||
"""
|
||||
x = 42
|
||||
""".trimIndent().trim()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a module that loads a package`(@TempDir tempDir: Path) {
|
||||
val cacheDir = tempDir.resolve("cache")
|
||||
val pklFile = tempDir.resolve("test.pkl")
|
||||
pklFile.toFile().writeText(
|
||||
"""
|
||||
@@ -422,11 +500,8 @@ class EmbeddedExecutorTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
PackageServer.ensureStarted()
|
||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val result = executor.use {
|
||||
it.evaluatePath(pklFile,
|
||||
ExecutorOptions(
|
||||
val result = currentExecutor.evaluatePath(pklFile,
|
||||
ExecutorOptions2(
|
||||
listOf("file:", "package:", "https:"),
|
||||
listOf("prop:", "package:", "https:"),
|
||||
mapOf(),
|
||||
@@ -435,10 +510,11 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
ExecutorOptions.defaultModuleCacheDir(),
|
||||
null)
|
||||
cacheDir,
|
||||
null,
|
||||
listOf(FileTestUtils.selfSignedCertificate),
|
||||
listOf())
|
||||
)
|
||||
}
|
||||
assertThat(result.trim()).isEqualTo("""
|
||||
chirpy {
|
||||
name = "Chirpy"
|
||||
@@ -447,10 +523,14 @@ class EmbeddedExecutorTest {
|
||||
}
|
||||
}
|
||||
""".trimIndent())
|
||||
|
||||
// verify that cache was populated
|
||||
assertThat(cacheDir.toFile().list()).isNotEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `evaluate a project dependency`(@TempDir tempDir: Path) {
|
||||
@ParameterizedTest
|
||||
@MethodSource("getAllExecutionContexts")
|
||||
fun `evaluate a project dependency`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||
val cacheDir = tempDir.resolve("packages")
|
||||
PackageServer.populateCacheDir(cacheDir)
|
||||
val projectDir = tempDir.resolve("project/")
|
||||
@@ -495,10 +575,8 @@ class EmbeddedExecutorTest {
|
||||
result = Swallow
|
||||
""".trimIndent()
|
||||
)
|
||||
val executor = Executors.embedded(listOf(pklDistribution))
|
||||
val result = executor.use {
|
||||
it.evaluatePath(pklFile,
|
||||
ExecutorOptions(
|
||||
val result = context.executor.evaluatePath(pklFile,
|
||||
context.options(ExecutorOptions2(
|
||||
listOf("file:", "package:", "projectpackage:", "https:"),
|
||||
listOf("prop:", "package:", "projectpackage:", "https:"),
|
||||
mapOf(),
|
||||
@@ -508,9 +586,10 @@ class EmbeddedExecutorTest {
|
||||
null,
|
||||
null,
|
||||
cacheDir,
|
||||
projectDir)
|
||||
)
|
||||
}
|
||||
projectDir,
|
||||
listOf(),
|
||||
listOf())
|
||||
))
|
||||
assertThat(result).isEqualTo("""
|
||||
result {
|
||||
name = "Swallow"
|
||||
|
||||
Reference in New Issue
Block a user