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:
translatenix
2024-03-06 10:25:56 -08:00
committed by GitHub
parent 106743354c
commit 3f3dfdeb1e
79 changed files with 2376 additions and 395 deletions

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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"