Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
/**
* 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.cli.svm;
import org.graalvm.nativeimage.hosted.Feature;
import org.pkl.core.runtime.BaseModule;
/**
* This class is registered with native-image via a CLI option (see Gradle task `nativeExecutable`).
*/
@SuppressWarnings({"unused", "ResultOfMethodCallIgnored"})
public final class InitFeature implements Feature {
/**
* Enforce that {@link BaseModule#getModule()}'s static initializer completes before depending
* static initializers are invoked. This is necessary to avoid deadlocks in native-image's
* multi-threaded execution of static initializers. It's not clear at this point if multi-threaded
* initialization on the JVM could also deadlock, i.e., if this is a Pkl bug.
*/
public void duringSetup(DuringSetupAccess access) {
BaseModule.getModule();
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.TargetClass;
/**
* native-image can't determine how calls to {@link sun.misc.Unsafe#arrayBaseOffset(Class)} affect
* static fields in msgpack's {@code MessageBuffer}.
*
* <p>This informs the compiler which field to re-compute.
*/
@SuppressWarnings("unused")
@TargetClass(className = "org.msgpack.core.buffer.MessageBuffer")
final class MessagePackRecomputations {
@Alias
@RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayBaseOffset, declClass = byte[].class)
static int ARRAY_BYTE_BASE_OFFSET;
}

View File

@@ -0,0 +1,52 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
import java.util.Map;
/**
* Workaround to prevent the native-image build error "Detected a started Thread in the image
* heap.". The cause of this error is the use of {@link org.graalvm.polyglot.Context} in the
* (intentionally) statically reachable class {@link org.pkl.core.runtime.StdLibModule}.
*
* <p>A cleaner solution would be to have a separate {@link org.pkl.core.ast.builder.AstBuilder} for
* stdlib modules that produces a fully initialized module object without executing any Truffle
* nodes.
*
* <p>This class is automatically discovered by native-image; no registration is required.
*/
@SuppressWarnings({"unused", "ClassName"})
@TargetClass(
className = "com.oracle.truffle.polyglot.PolyglotContextImpl",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class PolyglotContextImplTarget {
@Alias
@RecomputeFieldValue(kind = Kind.NewInstance, declClassName = "java.util.HashMap")
public Map<?, ?> threads;
@Alias
@RecomputeFieldValue(kind = Kind.Reset)
public WeakAssumedValueTarget singleThreadValue;
@Alias
@RecomputeFieldValue(kind = Kind.Reset)
public PolyglotThreadInfoTarget cachedThreadInfo;
}

View File

@@ -0,0 +1,25 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
/** Makes non-public class PolyglotThreadInfo usable above. */
@TargetClass(
className = "com.oracle.truffle.polyglot.PolyglotThreadInfo",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class PolyglotThreadInfoTarget {}

View File

@@ -0,0 +1,33 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
import java.util.Map;
@SuppressWarnings("unused")
@TargetClass(
className = "com.oracle.truffle.api.impl.ThreadLocalHandshake",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class ThreadLocalHandshakeTarget {
@Alias
@RecomputeFieldValue(kind = Kind.NewInstance, declClassName = "java.util.HashMap")
static Map<?, ?> SAFEPOINTS;
}

View File

@@ -0,0 +1,25 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
/** Makes non-public class WeakAssumedValue usable. */
@TargetClass(
className = "com.oracle.truffle.polyglot.WeakAssumedValue",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class WeakAssumedValueTarget {}

View File

@@ -0,0 +1,48 @@
/**
* 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.cli
import java.nio.file.Files
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliBaseOptions.Companion.getProjectFile
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME
abstract class CliAbstractProjectCommand(
cliOptions: CliBaseOptions,
private val projectDirs: List<Path>
) : CliCommand(cliOptions) {
protected val normalizedProjectFiles: List<Path> by lazy {
if (projectDirs.isEmpty()) {
val projectFile =
cliOptions.normalizedWorkingDir.getProjectFile(cliOptions.normalizedRootDir)
?: throw CliException(
"No project visible to the working directory. Ensure there is a PklProject file in the workspace, or provide an explicit project directory as an argument."
)
return@lazy listOf(projectFile.normalize())
}
projectDirs.map { dir ->
val projectFile = dir.resolve(PKL_PROJECT_FILENAME)
if (!Files.exists(projectFile)) {
throw CliException("Directory $dir does not contain a PklProject file.")
}
projectFile.normalize()
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.cli
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.core.packages.PackageResolver
import org.pkl.core.packages.PackageUri
class CliDownloadPackageCommand(
baseOptions: CliBaseOptions,
private val packageUris: List<PackageUri>,
private val noTranstive: Boolean
) : CliCommand(baseOptions) {
override fun doRun() {
if (moduleCacheDir == null) {
throw CliException("Cannot download packages because no cache directory is specified.")
}
val packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir)
val errors = mutableMapOf<PackageUri, Throwable>()
for (pkg in packageUris) {
try {
packageResolver.downloadPackage(pkg, pkg.checksums, noTranstive)
} catch (e: Throwable) {
errors[pkg] = e
}
}
when (errors.size) {
0 -> return
1 -> throw CliException(errors.values.single().message!!)
else ->
throw CliException(
buildString {
appendLine("Failed to download some packages.")
for ((uri, error) in errors) {
appendLine()
appendLine("Failed to download $uri because:")
appendLine("${error.message}")
}
}
)
}
}
}

View File

@@ -0,0 +1,236 @@
/**
* 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.cli
import java.io.File
import java.io.Reader
import java.io.Writer
import java.net.URI
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.commons.createParentDirectories
import org.pkl.commons.currentWorkingDir
import org.pkl.commons.writeString
import org.pkl.core.EvaluatorBuilder
import org.pkl.core.ModuleSource
import org.pkl.core.PklException
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModulePathResolver
import org.pkl.core.runtime.ModuleResolver
import org.pkl.core.runtime.VmException
import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils
private data class OutputFile(val pathSpec: String, val moduleUri: URI)
/** API equivalent of the Pkl command-line evaluator. */
class CliEvaluator
@JvmOverloads
constructor(
private val options: CliEvaluatorOptions,
// use System.{in,out}() rather than System.console()
// because the latter returns null when output is sent through a unix pipe
private val consoleReader: Reader = System.`in`.reader(),
private val consoleWriter: Writer = System.out.writer(),
) : CliCommand(options.base) {
/**
* Output files for the modules to be evaluated. Returns `null` if `options.outputPath` is `null`.
* Multiple modules may be mapped to the same output file, in which case their outputs are
* concatenated with [CliEvaluatorOptions.moduleOutputSeparator].
*/
@Suppress("MemberVisibilityCanBePrivate")
val outputFiles: Set<File>? by lazy {
fileOutputPaths?.values?.mapTo(mutableSetOf(), Path::toFile)
}
/**
* Output directories for the modules to be evaluated. Returns `null` if
* `options.multipleFileOutputPath` is `null`.
*/
@Suppress("MemberVisibilityCanBePrivate")
val outputDirectories: Set<File>? by lazy {
directoryOutputPaths?.values?.mapTo(mutableSetOf(), Path::toFile)
}
/** The file output path */
val fileOutputPaths: Map<URI, Path>? by lazy {
if (options.multipleFileOutputPath != null) return@lazy null
options.outputPath?.let { resolveOutputPaths(it) }
}
private val directoryOutputPaths: Map<URI, Path>? by lazy {
options.multipleFileOutputPath?.let { resolveOutputPaths(it) }
}
/**
* Evaluates source modules according to [options].
*
* If [CliEvaluatorOptions.outputPath] is set, each module's `output.text` is written to the
* module's [output file][outputFiles]. If [CliEvaluatorOptions.multipleFileOutputPath] is set,
* each module's `output.files` are written to the module's [output directory][outputDirectories].
* Otherwise, each module's `output.text` is written to [consoleWriter] (which defaults to
* standard out).
*
* Throws [CliException] in case of an error.
*/
override fun doRun() {
val builder = evaluatorBuilder()
try {
if (options.multipleFileOutputPath != null) {
writeMultipleFileOutput(builder)
} else {
writeOutput(builder)
}
} finally {
ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)
}
}
private fun resolveOutputPaths(pathStr: String): Map<URI, Path> {
val moduleUris = options.base.normalizedSourceModules
val workingDir = options.base.normalizedWorkingDir
// used just to resolve the `%{moduleName}` placeholder
val moduleResolver = ModuleResolver(moduleKeyFactories(ModulePathResolver.empty()))
return moduleUris.associateWith { uri ->
val moduleDir: String? =
IoUtils.toPath(uri)?.let { workingDir.relativize(it.parent).toString().ifEmpty { "." } }
val moduleKey =
try {
moduleResolver.resolve(uri)
} catch (e: VmException) {
throw e.toPklException(stackFrameTransformer)
}
val substituted =
pathStr
.replace("%{moduleName}", IoUtils.inferModuleName(moduleKey))
.replace("%{outputFormat}", options.outputFormat ?: "%{outputFormat}")
.replace("%{moduleDir}", moduleDir ?: "%{moduleDir}")
if (substituted.contains("%{moduleDir}")) {
throw PklException(
"Cannot substitute output path placeholder `%{moduleDir}` " +
"because module `$uri` does not have a file system path."
)
}
val absolutePath = workingDir.resolve(substituted).normalize()
absolutePath
}
}
/** Renders each module's `output.text`, writing it to the specified output file. */
private fun writeOutput(builder: EvaluatorBuilder) {
val evaluator = builder.setOutputFormat(options.outputFormat).build()
evaluator.use {
val outputFiles = fileOutputPaths
if (outputFiles != null) {
// files that we've written non-empty output to
// YamlRenderer produces empty output if `isStream` is true and `output.value` is empty
// collection
val writtenFiles = mutableSetOf<Path>()
for ((moduleUri, outputFile) in outputFiles) {
val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateExpressionString(moduleSource, options.expression)
outputFile.createParentDirectories()
if (!writtenFiles.contains(outputFile)) {
// write file even if output is empty to overwrite output from previous runs
outputFile.writeString(output)
if (output.isNotEmpty()) {
writtenFiles.add(outputFile)
}
} else {
if (output.isNotEmpty()) {
outputFile.writeString(
options.moduleOutputSeparator + IoUtils.getLineSeparator(),
Charsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND
)
outputFile.writeString(
output,
Charsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND
)
}
}
}
} else {
var outputWritten = false
for (moduleUri in options.base.normalizedSourceModules) {
val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateExpressionString(moduleSource, options.expression)
if (output.isNotEmpty()) {
if (outputWritten) consoleWriter.appendLine(options.moduleOutputSeparator)
consoleWriter.write(output)
consoleWriter.flush()
outputWritten = true
}
}
}
}
}
private fun toModuleSource(uri: URI, reader: Reader) =
if (uri == VmUtils.REPL_TEXT_URI) ModuleSource.create(uri, reader.readText())
else ModuleSource.uri(uri)
/**
* Renders each module's `output.files`, writing each entry as a file into the specified output
* directory.
*/
private fun writeMultipleFileOutput(builder: EvaluatorBuilder) {
val outputDirs = directoryOutputPaths!!
val writtenFiles = mutableMapOf<Path, OutputFile>()
for ((moduleUri, outputDir) in outputDirs) {
val evaluator = builder.setOutputFormat(options.outputFormat).build()
if (outputDir.exists() && !outputDir.isDirectory()) {
throw CliException("Output path `$outputDir` exists and is not a directory.")
}
val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateOutputFiles(moduleSource)
for ((pathSpec, fileOutput) in output) {
val resolvedPath = outputDir.resolve(pathSpec).normalize()
val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath
if (!realPath.startsWith(outputDir)) {
throw CliException(
"Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$outputDir`."
)
}
val previousOutput = writtenFiles[realPath]
if (previousOutput != null) {
throw CliException(
"Output file conflict: `output.files` entries `\"${previousOutput.pathSpec}\"` in module `${previousOutput.moduleUri}` and `\"$pathSpec\"` in module `$moduleUri` resolve to the same file path `$realPath`."
)
}
if (realPath.isDirectory()) {
throw CliException(
"Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is a directory."
)
}
writtenFiles[realPath] = OutputFile(pathSpec, moduleUri)
realPath.createParentDirectories()
realPath.writeString(fileOutput.text)
consoleWriter.write(currentWorkingDir.relativize(resolvedPath).toString() + "\n")
consoleWriter.flush()
}
}
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.cli
import org.pkl.commons.cli.CliBaseOptions
/** Configuration options for [CliEvaluator]. */
data class CliEvaluatorOptions(
/** Base options shared between CLI commands. */
val base: CliBaseOptions,
/**
* The file path where the output file is placed. If multiple source modules are given,
* placeholders can be used to map them to different output files. If multiple modules are mapped
* to the same output file, their outputs are concatenated. Currently, the only available
* concatenation strategy is to separate outputs with `---`, as in a YAML stream.
*
* The following placeholders are supported:
* - `%{moduleDir}` The directory path of the module, relative to the working directory. Only
* available when evaluating file-based modules.
* - `%{moduleName}` The simple module name as inferred from the module URI. For hierarchical
* URIs, this is the last path segment without file extension.
* - `%{outputFormat}` The requested output format. Only available if `outputFormat` is non-null.
*
* If [CliBaseOptions.workingDir] corresponds to a file system path, relative output paths are
* resolved against that path. Otherwise, relative output paths are not allowed.
*
* If `null`, output is written to the console.
*/
val outputPath: String? = null,
/**
* The output format to generate.
*
* The default output renderer for a module supports the following formats:
* - `"json"`
* - `"jsonnet"`
* - `"pcf"` (default)
* - `"plist"`
* - `"properties"`
* - `"textproto"`
* - `"xml"`
* - `"yaml"`
*/
val outputFormat: String? = null,
/** The separator to use when multiple module outputs are written to the same location. */
val moduleOutputSeparator: String = "---",
/**
* The directory where a module's output files are placed.
*
* Setting this option causes Pkl to evaluate `output.files` instead of `output.text`, and write
* files using each entry's key as the file path relative to [multipleFileOutputPath], and each
* value's `text` property as the file's contents.
*/
val multipleFileOutputPath: String? = null,
/**
* The expression to evaluate within the module.
*
* If set, the said expression is evaluated under the context of the enclosing module.
*
* If unset, the module's `output.text` property evaluated.
*/
val expression: String = "output.text",
) {
companion object {
val defaults = CliEvaluatorOptions(CliBaseOptions())
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.cli
import java.io.Writer
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.commons.cli.CliTestException
import org.pkl.commons.cli.CliTestOptions
import org.pkl.core.project.Project
import org.pkl.core.project.ProjectPackager
import org.pkl.core.util.ErrorMessages
class CliProjectPackager(
baseOptions: CliBaseOptions,
projectDirs: List<Path>,
private val testOptions: CliTestOptions,
private val outputPath: String,
private val skipPublishCheck: Boolean,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer()
) : CliAbstractProjectCommand(baseOptions, projectDirs) {
private fun runApiTests(project: Project) {
val apiTests = project.`package`!!.apiTests
if (apiTests.isEmpty()) return
val normalizeApiTests = apiTests.map { project.projectDir.resolve(it).toUri() }
val testRunner =
CliTestRunner(
cliOptions.copy(sourceModules = normalizeApiTests, projectDir = project.projectDir),
testOptions = testOptions,
consoleWriter = consoleWriter,
errWriter = errWriter,
)
try {
testRunner.run()
} catch (e: CliTestException) {
throw CliException(ErrorMessages.create("packageTestsFailed", project.`package`!!.uri))
}
}
override fun doRun() {
val projects = buildList {
for (projectFile in normalizedProjectFiles) {
val project = loadProject(projectFile)
project.`package`
?: throw CliException(
ErrorMessages.create("noPackageDefinedByProject", project.projectFileUri)
)
runApiTests(project)
add(project)
}
}
// Require that all local projects are included
projects.forEach { proj ->
proj.dependencies.localDependencies.values.forEach { localDep ->
val projectDir = Path.of(localDep.projectFileUri).parent
if (projects.none { it.projectDir == projectDir }) {
throw CliException(
ErrorMessages.create("missingProjectInPackageCommand", proj.projectDir, projectDir)
)
}
}
}
ProjectPackager(
projects,
cliOptions.normalizedWorkingDir,
outputPath,
stackFrameTransformer,
securityManager,
skipPublishCheck,
consoleWriter
)
.createPackages()
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.cli
import java.io.Writer
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.core.SecurityManagers
import org.pkl.core.module.ProjectDependenciesManager
import org.pkl.core.packages.PackageResolver
import org.pkl.core.project.ProjectDependenciesResolver
class CliProjectResolver(
baseOptions: CliBaseOptions,
projectDirs: List<Path>,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer()
) : CliAbstractProjectCommand(baseOptions, projectDirs) {
override fun doRun() {
for (projectFile in normalizedProjectFiles) {
val project = loadProject(projectFile)
val packageResolver =
PackageResolver.getInstance(
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir
),
moduleCacheDir
)
val dependencies = ProjectDependenciesResolver(project, packageResolver, errWriter).resolve()
val depsFile =
projectFile.parent.resolve(ProjectDependenciesManager.PKL_PROJECT_DEPS_FILENAME).toFile()
depsFile.outputStream().use { dependencies.writeTo(it) }
consoleWriter.appendLine(depsFile.toString())
consoleWriter.flush()
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.cli
import org.pkl.cli.repl.Repl
import org.pkl.commons.cli.CliCommand
import org.pkl.core.Loggers
import org.pkl.core.SecurityManagers
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModulePathResolver
import org.pkl.core.repl.ReplServer
import org.pkl.core.resource.ResourceReaders
internal class CliRepl(private val options: CliEvaluatorOptions) : CliCommand(options.base) {
override fun doRun() {
ModulePathResolver(modulePath).use { modulePathResolver ->
// TODO: send options as command
val server =
ReplServer(
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir
),
Loggers.stdErr(),
listOf(
ModuleKeyFactories.standardLibrary,
ModuleKeyFactories.modulePath(modulePathResolver)
) +
ModuleKeyFactories.fromServiceProviders() +
listOf(
ModuleKeyFactories.file,
ModuleKeyFactories.pkg,
ModuleKeyFactories.projectpackage,
ModuleKeyFactories.genericUrl
),
listOf(
ResourceReaders.environmentVariable(),
ResourceReaders.externalProperty(),
ResourceReaders.modulePath(modulePathResolver),
ResourceReaders.file(),
ResourceReaders.http(),
ResourceReaders.https(),
ResourceReaders.pkg(),
ResourceReaders.projectpackage()
),
environmentVariables,
externalProperties,
moduleCacheDir,
project?.dependencies,
options.outputFormat,
options.base.normalizedWorkingDir,
stackFrameTransformer
)
Repl(options.base.normalizedWorkingDir, server).run()
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.cli
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.server.MessageTransports
import org.pkl.server.ProtocolException
import org.pkl.server.Server
class CliServer(options: CliBaseOptions) : CliCommand(options) {
override fun doRun() =
try {
val server = Server(MessageTransports.stream(System.`in`, System.out))
server.use { it.start() }
} catch (e: ProtocolException) {
throw CliException(e.message!!)
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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.cli
import java.io.Writer
import org.pkl.commons.cli.*
import org.pkl.core.EvaluatorBuilder
import org.pkl.core.ModuleSource.uri
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.stdlib.test.report.JUnitReport
import org.pkl.core.stdlib.test.report.SimpleReport
import org.pkl.core.util.ErrorMessages
class CliTestRunner
@JvmOverloads
constructor(
private val options: CliBaseOptions,
private val testOptions: CliTestOptions,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer()
) : CliCommand(options) {
override fun doRun() {
val builder = evaluatorBuilder()
try {
evalTest(builder)
} finally {
ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)
}
}
private fun evalTest(builder: EvaluatorBuilder) {
val sources =
options.normalizedSourceModules.ifEmpty { project?.tests?.map { it.toUri() } }
?:
// keep in sync with error message thrown by clikt
throw CliException(
"""
Usage: pkl test [OPTIONS] <modules>...
Error: Missing argument "<modules>"
"""
.trimIndent()
)
val evaluator = builder.build()
evaluator.use {
var failed = false
val moduleNames = mutableSetOf<String>()
for (moduleUri in sources) {
try {
val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite)
if (!failed) {
failed = results.failed()
}
SimpleReport().report(results, consoleWriter)
consoleWriter.flush()
val junitDir = testOptions.junitDir
if (junitDir != null) {
junitDir.toFile().mkdirs()
val moduleName = "${results.moduleName}.xml"
if (moduleName in moduleNames) {
throw RuntimeException(
"""
Cannot generate JUnit report for $moduleUri.
A report with the same name was already generated.
To fix, provide a different name for this module by adding a module header.
"""
.trimIndent()
)
}
moduleNames += moduleName
JUnitReport().reportToPath(results, junitDir.resolve(moduleName))
}
} catch (ex: Exception) {
errWriter.appendLine("Error evaluating module ${moduleUri.path}:")
errWriter.write(ex.message ?: "")
if (moduleUri != sources.last()) {
errWriter.appendLine()
}
errWriter.flush()
failed = true
}
}
if (failed) {
throw CliTestException(ErrorMessages.create("testsFailed"))
}
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.
*/
@file:JvmName("Main")
package org.pkl.cli
import com.github.ajalt.clikt.core.subcommands
import org.pkl.cli.commands.*
import org.pkl.commons.cli.CliMain
import org.pkl.commons.cli.cliMain
import org.pkl.core.Release
/** Main method of the Pkl CLI (command-line evaluator and REPL). */
internal fun main(args: Array<String>) {
val version = Release.current().versionInfo()
val helpLink = "${Release.current().documentation().homepage()}pkl-cli/index.html#usage"
val commands =
arrayOf(
EvalCommand(helpLink),
ReplCommand(helpLink),
ServerCommand(helpLink),
TestCommand(helpLink),
ProjectCommand(helpLink),
DownloadPackageCommand(helpLink)
)
val cmd = RootCommand("pkl", version, helpLink).subcommands(*commands)
cliMain {
if (CliMain.compat == "alpine") {
// Alpine's main thread has a prohibitively small stack size by default;
// https://github.com/oracle/graal/issues/3398
var throwable: Throwable? = null
Thread(null, { cmd.main(args) }, "alpineMain", 10000000).apply {
setUncaughtExceptionHandler { _, t -> throwable = t }
start()
join()
}
throwable?.let { throw it }
} else {
cmd.main(args)
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import org.pkl.cli.CliDownloadPackageCommand
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.single
import org.pkl.core.packages.PackageUri
class DownloadPackageCommand(helpLink: String) :
BaseCommand(
name = "download-package",
helpLink = helpLink,
help =
"""
Download package(s)
This command downloads the specified packages to the cache directory.
If the package already exists in the cache directory, this command is a no-op.
Examples:
```
# Download two packages
$ pkl download-package package://example.com/package1@1.0.0 package://example.com/package2@1.0.0
```
"""
.trimIndent()
) {
private val projectOptions by ProjectOptions()
private val packageUris: List<PackageUri> by
argument("<package>", "The package URIs to download")
.convert { PackageUri(it) }
.multiple(required = true)
private val noTransitive: Boolean by
option(
names = arrayOf("--no-transitive"),
help = "Skip downloading transitive dependencies of a package"
)
.single()
.flag()
override fun run() {
CliDownloadPackageCommand(
baseOptions.baseOptions(emptyList(), projectOptions),
packageUris,
noTransitive
)
.run()
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.validate
import org.pkl.cli.CliEvaluator
import org.pkl.cli.CliEvaluatorOptions
import org.pkl.commons.cli.commands.ModulesCommand
import org.pkl.commons.cli.commands.single
class EvalCommand(helpLink: String) :
ModulesCommand(
name = "eval",
help = "Render pkl module(s)",
helpLink = helpLink,
) {
private val outputPath: String? by
option(
names = arrayOf("-o", "--output-path"),
metavar = "<path>",
help = "File path where the output file is placed."
)
.single()
private val moduleOutputSeparator: String by
option(
names = arrayOf("--module-output-separator"),
metavar = "<string>",
help =
"Separator to use when multiple module outputs are written to the same file. (default: ---)"
)
.single()
.default("---")
private val expression: String? by
option(
names = arrayOf("-x", "--expression"),
metavar = "<expression>",
help = "Expression to be evaluated within the module."
)
.single()
private val multipleFileOutputPath: String? by
option(
names = arrayOf("-m", "--multiple-file-output-path"),
metavar = "<path>",
help = "Directory where a module's multiple file output is placed."
)
.single()
.validate {
if (outputPath != null || expression != null) {
fail("Option is mutually exclusive with -o, --output-path and -x, --expression.")
}
}
// hidden option used by the native tests
private val testMode: Boolean by
option(names = arrayOf("--test-mode"), help = "Internal test mode", hidden = true).flag()
override fun run() {
val options =
CliEvaluatorOptions(
base = baseOptions.baseOptions(modules, projectOptions, testMode = testMode),
outputPath = outputPath,
outputFormat = baseOptions.format,
moduleOutputSeparator = moduleOutputSeparator,
multipleFileOutputPath = multipleFileOutputPath,
expression = expression ?: CliEvaluatorOptions.defaults.expression
)
CliEvaluator(options).run()
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path
import org.pkl.cli.CliProjectPackager
import org.pkl.cli.CliProjectResolver
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.TestOptions
import org.pkl.commons.cli.commands.single
class ProjectCommand(helpLink: String) :
NoOpCliktCommand(
name = "project",
help = "Run commands related to projects",
epilog = "For more information, visit $helpLink"
) {
init {
subcommands(ResolveCommand(helpLink), PackageCommand(helpLink))
}
companion object {
class ResolveCommand(helpLink: String) :
BaseCommand(
name = "resolve",
helpLink = helpLink,
help =
"""
Resolve dependencies for project(s)
This command takes the `dependencies` of `PklProject`s, and writes the
resolved versions to `PklProject.deps.json` files.
Examples:
```
# Search the current working directory for a project, and resolve its dependencies.
$ pkl project resolve
# Resolve dependencies for all projects within the `packages/` directory.
$ pkl project resolve packages/*/
```
""",
) {
private val projectDirs: List<Path> by
argument("<dir>", "The project directories to resolve dependencies for").path().multiple()
override fun run() {
CliProjectResolver(baseOptions.baseOptions(emptyList()), projectDirs).run()
}
}
private const val NEWLINE = '\u0085'
class PackageCommand(helpLink: String) :
BaseCommand(
name = "package",
helpLink = helpLink,
help =
"""
Verify package(s), and prepare package artifacts to be published.
This command runs a project's api tests, as defined by `apiTests` in `PklProject`.
Additionally, it verifies that all imports resolve to paths that are local to the project.
Finally, this command writes the folowing artifacts into the output directory specified by the output path.
- `name@version` - dependency metadata$NEWLINE
- `name@version.sha256` - dependency metadata's SHA-256 checksum$NEWLINE
- `name@version.zip` - package archive$NEWLINE
- `name@version.zip.sha256` - package archive's SHA-256 checksum
The output path option accepts the following placeholders:
- %{name}: The display name of the package$NEWLINE
- %{version}: The version of the package
If a project has local project dependencies, the depended upon project directories must also
be included as arguments to this command.
Examples:
```
# Search the current working directory for a project, and package it.
$ pkl project package
# Package all projects within the `packages/` directory.
$ pkl project package packages/*/
```
"""
.trimIndent(),
) {
private val testOptions by TestOptions()
private val projectDirs: List<Path> by
argument("<dir>", "The project directories to package").path().multiple()
private val outputPath: String by
option(
names = arrayOf("--output-path"),
help = "The directory to write artifacts to",
metavar = "<path>"
)
.single()
.default(".out/%{name}@%{version}")
private val skipPublishCheck: Boolean by
option(
names = arrayOf("--skip-publish-check"),
help = "Skip checking if a package has already been published with different contents",
)
.single()
.flag()
override fun run() {
CliProjectPackager(
baseOptions.baseOptions(emptyList()),
projectDirs,
testOptions.cliTestOptions,
outputPath,
skipPublishCheck
)
.run()
}
}
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import org.pkl.cli.CliEvaluatorOptions
import org.pkl.cli.CliRepl
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions
class ReplCommand(helpLink: String) :
BaseCommand(
name = "repl",
help = "Start a REPL session",
helpLink = helpLink,
) {
private val projectOptions by ProjectOptions()
override fun run() {
val options = CliEvaluatorOptions(base = baseOptions.baseOptions(emptyList(), projectOptions))
CliRepl(options).run()
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.parameters.options.versionOption
class RootCommand(name: String, version: String, helpLink: String) :
NoOpCliktCommand(
name = name,
printHelpOnEmptyArgs = true,
epilog = "For more information, visit $helpLink",
) {
init {
versionOption(version, names = setOf("-v", "--version"), message = { it })
context {
correctionSuggestor = { given, possible ->
if (!given.startsWith("-")) {
registeredSubcommands().map { it.commandName }
} else possible
}
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.core.CliktCommand
import org.pkl.cli.CliServer
import org.pkl.commons.cli.CliBaseOptions
class ServerCommand(helpLink: String) :
CliktCommand(
name = "server",
help = "Run as a server that communicates over standard input/output",
epilog = "For more information, visit $helpLink"
) {
override fun run() {
CliServer(CliBaseOptions()).run()
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import java.net.URI
import org.pkl.cli.CliTestRunner
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.TestOptions
class TestCommand(helpLink: String) :
BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) {
val modules: List<URI> by
argument(name = "<modules>", help = "Module paths or URIs to evaluate.")
.convert { parseModuleName(it) }
.multiple()
private val projectOptions by ProjectOptions()
private val testOptions by TestOptions()
override fun run() {
CliTestRunner(
options = baseOptions.baseOptions(modules, projectOptions),
testOptions = testOptions.cliTestOptions
)
.run()
}
}

View File

@@ -0,0 +1,219 @@
/**
* 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.cli.repl
import java.io.IOException
import java.net.URI
import java.nio.file.Path
import kotlin.io.path.deleteIfExists
import org.fusesource.jansi.Ansi
import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader.Option
import org.jline.reader.LineReaderBuilder
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.completer.AggregateCompleter
import org.jline.reader.impl.history.DefaultHistory
import org.jline.terminal.TerminalBuilder
import org.jline.utils.InfoCmp
import org.pkl.core.repl.ReplRequest
import org.pkl.core.repl.ReplResponse
import org.pkl.core.repl.ReplServer
import org.pkl.core.util.IoUtils
internal class Repl(workingDir: Path, private val server: ReplServer) {
private val terminal = TerminalBuilder.builder().apply { jansi(true) }.build()
private val history = DefaultHistory()
private val reader =
LineReaderBuilder.builder()
.apply {
history(history)
terminal(terminal)
completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir)))
option(Option.DISABLE_EVENT_EXPANSION, true)
variable(
org.jline.reader.LineReader.HISTORY_FILE,
(IoUtils.getPklHomeDir().resolve("repl-history"))
)
}
.build()
private var continuation = false
private var quit = false
private var nextRequestId = 0
fun run() {
// JLine 2 history file is incompatible with JLine 3
IoUtils.getPklHomeDir().resolve("repl-history.bin").deleteIfExists()
println(ReplMessages.welcome)
println()
var inputBuffer = ""
try {
while (!quit) {
val line =
try {
if (continuation) {
nextRequestId -= 1
reader.readLine(" ".repeat("pkl$nextRequestId> ".length))
} else {
reader.readLine("pkl$nextRequestId> ")
}
} catch (e: UserInterruptException) {
":quit"
} catch (e: EndOfFileException) {
":quit"
}
val input = line.trim()
if (input.isEmpty()) continue
if (continuation) {
inputBuffer = (inputBuffer + "\n" + input).trim()
continuation = false
} else {
inputBuffer = input
}
if (inputBuffer.startsWith(":")) {
executeCommand(inputBuffer)
} else {
evaluate(inputBuffer)
}
}
} finally {
try {
history.save()
} catch (ignored: IOException) {}
try {
terminal.close()
} catch (ignored: IOException) {}
}
}
private fun executeCommand(inputBuffer: String) {
val candidates = getMatchingCommands(inputBuffer)
when {
candidates.isEmpty() -> {
println("Unknown command: `${inputBuffer.drop(1)}`")
}
candidates.size > 1 -> {
print("Which of the following did you mean? ")
println(candidates.joinToString(separator = " ") { "`:${it.type}`" })
}
else -> {
doExecuteCommand(candidates.single())
}
}
}
private fun doExecuteCommand(command: ParsedCommand) {
when (command.type) {
Command.Clear -> clear()
Command.Examples -> examples()
Command.Force -> force(command)
Command.Help -> help()
Command.Load -> load(command)
Command.Quit -> quit()
Command.Reset -> reset()
}
}
private fun clear() {
terminal.puts(InfoCmp.Capability.clear_screen)
terminal.flush()
}
private fun examples() {
println(ReplMessages.examples)
}
private fun help() {
println(ReplMessages.help)
}
private fun quit() {
quit = true
}
private fun reset() {
server.handleRequest(ReplRequest.Reset(nextRequestId()))
clear()
nextRequestId = 0
}
private fun evaluate(inputBuffer: String) {
handleEvalRequest(ReplRequest.Eval(nextRequestId(), inputBuffer, false, false))
}
private fun loadModule(uri: URI) {
handleEvalRequest(ReplRequest.Load(nextRequestId(), uri))
}
private fun force(command: ParsedCommand) {
handleEvalRequest(ReplRequest.Eval(nextRequestId(), command.arg, false, true))
}
private fun load(command: ParsedCommand) {
loadModule(IoUtils.toUri(command.arg))
}
private fun handleEvalRequest(request: ReplRequest) {
val responses = server.handleRequest(request)
for (response in responses) {
when (response) {
is ReplResponse.EvalSuccess -> {
println(response.result)
}
is ReplResponse.EvalError -> {
println(response.message)
}
is ReplResponse.InternalError -> {
throw response.cause
}
is ReplResponse.IncompleteInput -> {
assert(responses.size == 1)
continuation = true
}
else -> throw IllegalStateException("Unexpected response: $response")
}
}
}
private fun nextRequestId(): String = "pkl$nextRequestId".apply { nextRequestId += 1 }
private fun print(msg: String) {
terminal.writer().print(highlight(msg))
}
private fun println(msg: String = "") {
terminal.writer().println(highlight(msg))
}
private fun highlight(str: String): String {
val ansi = Ansi.ansi()
var normal = true
for (part in str.split("`", "```")) {
ansi.a(part)
normal = !normal
if (!normal) ansi.bold() else ansi.boldOff()
}
ansi.reset()
return ansi.toString()
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.cli.repl
private val cmdRegex = Regex(":(\\p{Alpha}*)(\\p{Space}*)(.*)", RegexOption.DOT_MATCHES_ALL)
internal fun getMatchingCommands(input: String): List<ParsedCommand> {
val match = cmdRegex.matchEntire(input) ?: return listOf()
val (cmd, ws, arg) = match.destructured
return Command.values()
.filter { it.toString().lowercase().startsWith(cmd) }
.map { ParsedCommand(it, cmd, ws, arg) }
}
internal data class ParsedCommand(
val type: Command,
val cmd: String,
val ws: String,
val arg: String
)
internal enum class Command {
Clear,
Examples,
Force,
Help,
Load,
Quit,
Reset;
override fun toString() = name.lowercase()
}

View File

@@ -0,0 +1,182 @@
/**
* 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.cli.repl
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import org.jline.reader.Candidate
import org.jline.reader.Completer
import org.jline.reader.LineReader
import org.jline.reader.ParsedLine
import org.jline.terminal.Terminal
import org.jline.utils.AttributedStringBuilder
import org.jline.utils.OSUtils
import org.jline.utils.StyleResolver
/**
* Originally copied from:
* https://github.com/jline/jline3/blob/jline-parent-3.21.0/builtins/src/main/java/org/jline/builtins/Completers.java
*
* Reasons for copying this class instead of adding jline-builtins dependency:
* - Adding the dependency breaks native-image build (at least when using build-time initialization,
* might work with some config).
* - Completers.FileNameCompleter is the only class we currently use.
*/
internal abstract class JLineFileNameCompleter : Completer {
override fun complete(
reader: LineReader,
commandLine: ParsedLine,
candidates: MutableList<Candidate>
) {
val buffer = commandLine.word().substring(0, commandLine.wordCursor())
val current: Path
val curBuf: String
val sep = getSeparator(reader.isSet(LineReader.Option.USE_FORWARD_SLASH))
val lastSep = buffer.lastIndexOf(sep)
try {
if (lastSep >= 0) {
curBuf = buffer.substring(0, lastSep + 1)
current =
if (curBuf.startsWith("~")) {
if (curBuf.startsWith("~$sep")) {
userHome.resolve(curBuf.substring(2))
} else {
userHome.parent.resolve(curBuf.substring(1))
}
} else {
userDir.resolve(curBuf)
}
} else {
curBuf = ""
current = userDir
}
try {
Files.newDirectoryStream(current) { accept(it) }
.use { directory ->
directory.forEach { path ->
val value = curBuf + path.fileName.toString()
if (Files.isDirectory(path)) {
candidates.add(
Candidate(
value + if (reader.isSet(LineReader.Option.AUTO_PARAM_SLASH)) sep else "",
getDisplay(reader.terminal, path, resolver, sep),
null,
null,
if (reader.isSet(LineReader.Option.AUTO_REMOVE_SLASH)) sep else null,
null,
false
)
)
} else {
candidates.add(
Candidate(
value,
getDisplay(reader.terminal, path, resolver, sep),
null,
null,
null,
null,
true
)
)
}
}
}
} catch (ignored: IOException) {}
} catch (ignored: Exception) {}
}
protected open fun accept(path: Path): Boolean {
return try {
!Files.isHidden(path)
} catch (e: IOException) {
false
}
}
protected open val userDir: Path
get() = Path.of(System.getProperty("user.dir"))
private val userHome: Path
get() = Path.of(System.getProperty("user.home"))
private fun getSeparator(useForwardSlash: Boolean): String {
return if (useForwardSlash) "/" else userDir.fileSystem.separator
}
private fun getDisplay(
terminal: Terminal,
path: Path,
resolver: StyleResolver,
separator: String
): String {
val builder = AttributedStringBuilder()
val name = path.fileName.toString()
val index = name.lastIndexOf(".")
val type = if (index != -1) ".*" + name.substring(index) else null
if (Files.isSymbolicLink(path)) {
builder.styled(resolver.resolve(".ln"), name).append("@")
} else if (Files.isDirectory(path)) {
builder.styled(resolver.resolve(".di"), name).append(separator)
} else if (Files.isExecutable(path) && !OSUtils.IS_WINDOWS) {
builder.styled(resolver.resolve(".ex"), name).append("*")
} else if (type != null && resolver.resolve(type).style != 0L) {
builder.styled(resolver.resolve(type), name)
} else if (Files.isRegularFile(path)) {
builder.styled(resolver.resolve(".fi"), name)
} else {
builder.append(name)
}
return builder.toAnsi(terminal)
}
companion object {
private val resolver = StyleResolver { name ->
when (name) {
// imitate org.jline.builtins.Styles.DEFAULT_LS_COLORS
"di" -> "1;91"
"ex" -> "1;92"
"ln" -> "1;96"
"fi" -> null
else -> null
}
}
}
}
internal class FileCompleter(override val userDir: Path) : JLineFileNameCompleter() {
override fun complete(
reader: LineReader,
commandLine: ParsedLine,
candidates: MutableList<Candidate>
) {
val loadCmd =
getMatchingCommands(commandLine.line()).find { it.type == Command.Load && it.ws.isNotEmpty() }
if (loadCmd != null) {
super.complete(reader, commandLine, candidates)
}
}
}
internal object CommandCompleter : Completer {
private val commandCandidates: List<Candidate> =
Command.values().map { Candidate(":" + it.toString().lowercase()) }
override fun complete(reader: LineReader, line: ParsedLine, candidates: MutableList<Candidate>) {
if (line.wordIndex() == 0) candidates.addAll(commandCandidates)
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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.cli.repl
import org.pkl.core.Release
internal object ReplMessages {
val welcome =
"""
Welcome to Pkl ${Release.current().version()}.
Type an expression to have it evaluated.
Type `:help` or `:examples` for more information.
"""
.trimIndent()
val help =
"""
`<expr>` Evaluate <expr> and print the result. `1 + 3`
`<name> = <expr>` Evaluate <expr> and assign the result to property <name>. `msg = "howdy"`
`:clear` Clear the screen.
`:examples` Show code examples (use copy and paste to run them).
`:force <expr>` Force eager evaluation of a value.
`:help` Show this help.
`:load <file>` Load <file> from local file system. `:load path/to/config.pkl`
`:quit` Quit this program.
`:reset` Reset the environment to its initial state.
Tips:
* Commands can be abbreviated. `:h`
* Commands can be completed. `:<TAB>`
* File paths can be completed. `:load <TAB>`
* Expressions can be completed. `"hello".re<TAB>`
* Multiple declarations and expressions can be evaluated at once. `a = 1; b = a + 2`
* Incomplete input will be continued on the next line.
* Multi-line programs can be copy-pasted into the REPL.
"""
.trimIndent()
val examples: String =
"""
Expressions:
`2 + 3 * 4`
Strings:
`"Hello, " + "World!"`
Properties:
`timeout = 5.min; timeout`
Objects:
```pigeon {
name = "Pigeon"
fullName = "\(name) Bird"
age = 42
address {
street = "Landers St."
}
}
pigeon.fullName
hobbies {
"Swimming"
"Dancing"
"Surfing"
}
hobbies[1]
prices {
["Apple"] = 1.5
["Orange"] = 5
["Banana"] = 2
}
prices["Banana"]```
Inheritance:
```parrot = (pigeon) {
name = "Parrot"
age = 41
}
:force parrot```
For more examples, see the Language Reference${if (isMacOs()) " (Command+Double-click the link below)" else ""}:
${Release.current().documentation().homepage()}language-reference/
"""
.trimIndent()
private fun isMacOs() = System.getProperty("os.name").equals("Mac OS X", ignoreCase = true)
}

View File

@@ -0,0 +1,34 @@
/**
* This package contains source code from:
*
* <p>https://github.com/jline/jline3
*
* <p>Original license:
*
* <p>Copyright (c) 2002-2018, the original author or authors. All rights reserved.
*
* <p>https://opensource.org/licenses/BSD-3-Clause
*
* <p>Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* <p>Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
*
* <p>Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials provided with
* the distribution.
*
* <p>Neither the name of JLine nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* <p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.pkl.cli.repl;

View File

@@ -0,0 +1,7 @@
amends "pkl:Project"
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
}

View File

@@ -0,0 +1,229 @@
/**
* 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.cli
import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
import org.pkl.core.packages.PackageUri
class CliDownloadPackageCommandTest {
companion object {
@BeforeAll
@JvmStatic
fun beforeAll() {
PackageServer.ensureStarted()
}
}
@Test
fun `download packages`(@TempDir tempDir: Path) {
val cmd =
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris =
listOf(
PackageUri("package://localhost:12110/birds@0.5.0"),
PackageUri("package://localhost:12110/fruit@1.0.5"),
PackageUri("package://localhost:12110/fruit@1.1.0")
),
noTranstive = true
)
cmd.run()
assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.json")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.1.0/fruit@1.1.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.1.0/fruit@1.1.0.json")).exists()
}
@Test
fun `download packages with cache dir set by project`(@TempDir tempDir: Path) {
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
evaluatorSettings {
moduleCacheDir = ".my-cache"
}
"""
.trimIndent()
)
val cmd =
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris = listOf(PackageUri("package://localhost:12110/birds@0.5.0")),
noTranstive = true
)
cmd.run()
assertThat(tempDir.resolve(".my-cache/package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip"))
.exists()
assertThat(tempDir.resolve(".my-cache/package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json"))
.exists()
}
@Test
fun `download package while specifying checksum`(@TempDir tempDir: Path) {
val cmd =
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris =
listOf(
PackageUri(
"package://localhost:12110/birds@0.5.0::sha256:3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
),
),
noTranstive = true
)
cmd.run()
assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")).exists()
}
@Test
fun `download package with invalid checksum`(@TempDir tempDir: Path) {
val cmd =
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris =
listOf(
PackageUri("package://localhost:12110/birds@0.5.0::sha256:intentionallyBogusChecksum"),
),
noTranstive = true
)
assertThatCode { cmd.run() }
.hasMessage(
"""
Cannot download package `package://localhost:12110/birds@0.5.0` because the computed checksum for package metadata does not match the expected checksum.
Computed checksum: "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
Expected checksum: "intentionallyBogusChecksum"
Asset URL: "https://localhost:12110/birds@0.5.0"
"""
.trimIndent()
)
}
@Test
fun `disabling cacheing is an error`(@TempDir tempDir: Path) {
val cmd =
CliDownloadPackageCommand(
baseOptions = CliBaseOptions(workingDir = tempDir, noCache = true),
packageUris = listOf(PackageUri("package://localhost:12110/birds@0.5.0")),
noTranstive = true
)
assertThatCode { cmd.run() }
.hasMessage("Cannot download packages because no cache directory is specified.")
}
@Test
fun `download packages with bad checksum`(@TempDir tempDir: Path) {
val cmd =
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris = listOf(PackageUri("package://localhost:12110/badChecksum@1.0.0")),
noTranstive = true
)
assertThatCode { cmd.run() }
.hasMessageStartingWith(
"Cannot download package `package://localhost:12110/badChecksum@1.0.0` because the computed checksum does not match the expected checksum."
)
}
@Test
fun `download multiple failing packages`(@TempDir tempDir: Path) {
val cmd =
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris =
listOf(
PackageUri("package://localhost:12110/badChecksum@1.0.0"),
PackageUri("package://bogus.domain/notAPackage@1.0.0")
),
noTranstive = true
)
assertThatCode { cmd.run() }
.hasMessage(
"""
Failed to download some packages.
Failed to download package://localhost:12110/badChecksum@1.0.0 because:
Cannot download package `package://localhost:12110/badChecksum@1.0.0` because the computed checksum does not match the expected checksum.
Computed checksum: "0ec8a501e974802d0b71b8d58141e1e6eaa10bc2033e18200be3a978823d98aa"
Expected checksum: "intentionally bogus checksum"
Asset URL: "https://localhost:12110/badChecksum@1.0.0/badChecksum@1.0.0.zip"
Failed to download package://bogus.domain/notAPackage@1.0.0 because:
Exception when making request `GET https://bogus.domain/notAPackage@1.0.0`:
bogus.domain
"""
.trimIndent()
)
}
@Test
fun `download package, including transitive dependencies`(@TempDir tempDir: Path) {
CliDownloadPackageCommand(
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
packageUris = listOf(PackageUri("package://localhost:12110/birds@0.5.0")),
noTranstive = false
)
.run()
assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.json")).exists()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
/**
* 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.cli
import com.github.ajalt.clikt.core.BadParameterValue
import com.github.ajalt.clikt.core.subcommands
import java.nio.file.Path
import kotlin.io.path.createDirectory
import kotlin.io.path.createSymbolicLinkPointingTo
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.AssertionsForClassTypes.assertThatCode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.cli.commands.EvalCommand
import org.pkl.cli.commands.RootCommand
import org.pkl.commons.writeString
class CliMainTest {
private val evalCmd = EvalCommand("")
private val cmd = RootCommand("pkl", "pkl version 1", "").subcommands(evalCmd)
@Test
fun `duplicate CLI option produces meaningful errror message`(@TempDir tempDir: Path) {
val inputFile = tempDir.resolve("test.pkl").writeString("").toString()
assertThatCode {
cmd.parse(arrayOf("eval", "--output-path", "path1", "--output-path", "path2", inputFile))
}
.hasMessage("Invalid value for \"--output-path\": Option cannot be repeated")
assertThatCode {
cmd.parse(arrayOf("eval", "-o", "path1", "--output-path", "path2", inputFile))
}
.hasMessage("Invalid value for \"--output-path\": Option cannot be repeated")
}
@Test
fun `eval requires at least one file`() {
assertThatCode { cmd.parse(arrayOf("eval")) }.hasMessage("""Missing argument "<modules>"""")
}
@Test
fun `output to symlinked directory works`(@TempDir tempDir: Path) {
val code =
"""
x = 3
output {
value = x
renderer = new JsonRenderer {}
}
"""
.trimIndent()
val inputFile = tempDir.resolve("test.pkl").writeString(code).toString()
val outputFile = makeSymdir(tempDir, "out", "linkOut").resolve("test.pkl").toString()
assertThatCode { cmd.parse(arrayOf("eval", inputFile, "-o", outputFile)) }
.doesNotThrowAnyException()
}
@Test
fun `cannot have multiple output with -o or -x`(@TempDir tempDir: Path) {
val testIn = makeInput(tempDir)
val testOut = tempDir.resolve("test").toString()
val error =
"""Invalid value for "--multiple-file-output-path": Option is mutually exclusive with -o, --output-path and -x, --expression."""
assertThatCode { cmd.parse(arrayOf("eval", "-m", testOut, "-x", "x", testIn)) }
.hasMessage(error)
assertThatCode { cmd.parse(arrayOf("eval", "-m", testOut, "-o", "/tmp/test", testIn)) }
.hasMessage(error)
}
@Test
fun `showing version works`() {
assertThatCode { cmd.parse(arrayOf("--version")) }.hasMessage("pkl version 1")
}
@Test
fun `file paths get parsed into URIs`(@TempDir tempDir: Path) {
cmd.parse(arrayOf("eval", makeInput(tempDir, "my file.txt")))
val modules = evalCmd.baseOptions.baseOptions(evalCmd.modules).normalizedSourceModules
assertThat(modules).hasSize(1)
assertThat(modules[0].path).endsWith("my file.txt")
}
@Test
fun `invalid URIs are not accepted`() {
val ex = assertThrows<BadParameterValue> { cmd.parse(arrayOf("eval", "file:my file.txt")) }
assertThat(ex.message).contains("URI `file:my file.txt` has invalid syntax")
}
private fun makeInput(tempDir: Path, fileName: String = "test.pkl"): String {
val code = "x = 1"
return tempDir.resolve(fileName).writeString(code).toString()
}
private fun makeSymdir(baseDir: Path, name: String, linkName: String): Path {
val dir = baseDir.resolve(name)
val link = baseDir.resolve(linkName)
dir.createDirectory()
link.createSymbolicLinkPointingTo(dir)
return link
}
}

View File

@@ -0,0 +1,959 @@
/**
* 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.cli
import java.io.StringWriter
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.util.stream.Collectors
import kotlin.io.path.createDirectories
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.commons.cli.CliTestOptions
import org.pkl.commons.readString
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
import org.pkl.commons.writeString
import org.pkl.core.runtime.CertificateUtils
class CliProjectPackagerTest {
@Test
fun `missing PklProject when inferring a project dir`(@TempDir tempDir: Path) {
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
)
val err = assertThrows<CliException> { packager.run() }
assertThat(err).hasMessageStartingWith("No project visible to the working directory.")
}
@Test
fun `missing PklProject when explict dir is provided`(@TempDir tempDir: Path) {
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
)
val err = assertThrows<CliException> { packager.run() }
assertThat(err).hasMessageStartingWith("Directory $tempDir does not contain a PklProject file.")
}
@Test
fun `PklProject missing package section`(@TempDir tempDir: Path) {
tempDir
.resolve("PklProject")
.writeString(
"""
amends "pkl:Project"
"""
.trimIndent()
)
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
)
val err = assertThrows<CliException> { packager.run() }
assertThat(err)
.hasMessageStartingWith("No package was declared in project `${tempDir.toUri()}PklProject`.")
}
@Test
fun `failing apiTests`(@TempDir tempDir: Path) {
tempDir.writeFile(
"myTest.pkl",
"""
amends "pkl:test"
facts {
["1 == 2"] {
1 == 2
}
}
"""
.trimIndent()
)
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
apiTests { "myTest.pkl" }
}
"""
.trimIndent()
)
val buffer = StringWriter()
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = buffer
)
val err = assertThrows<CliException> { packager.run() }
assertThat(err).hasMessageContaining("because its API tests are failing")
assertThat(buffer.toString()).contains("1 == 2")
}
@Test
fun `passing apiTests`(@TempDir tempDir: Path) {
tempDir
.resolve("myTest.pkl")
.writeString(
"""
amends "pkl:test"
facts {
["1 == 1"] {
1 == 1
}
}
"""
.trimIndent()
)
tempDir
.resolve("PklProject")
.writeString(
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
apiTests { "myTest.pkl" }
}
"""
.trimIndent()
)
val buffer = StringWriter()
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = buffer
)
packager.run()
}
@Test
fun `apiTests that import dependencies`(@TempDir tempDir: Path) {
val cacheDir = tempDir.resolve("cache")
val projectDir = tempDir.resolve("myProject").createDirectories()
PackageServer.populateCacheDir(cacheDir)
projectDir
.resolve("myTest.pkl")
.writeString(
"""
amends "pkl:test"
import "@birds/Bird.pkl"
examples {
["Bird"] {
new Bird { name = "Finch"; favoriteFruit { name = "Tangerine" } }
}
}
"""
.trimIndent()
)
projectDir
.resolve("PklProject")
.writeString(
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
apiTests { "myTest.pkl" }
}
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
}
"""
.trimIndent()
)
projectDir
.resolve("PklProject.deps.json")
.writeString(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5"
}
}
}
}
"""
.trimIndent()
)
val buffer = StringWriter()
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = projectDir, moduleCacheDir = cacheDir),
listOf(projectDir),
CliTestOptions(),
".out",
skipPublishCheck = true,
consoleWriter = buffer
)
packager.run()
}
@Test
fun `generate package`(@TempDir tempDir: Path) {
val fooPkl =
tempDir.writeFile(
"a/b/foo.pkl",
"""
module foo
name: String
"""
.trimIndent()
)
val fooTxt =
tempDir.writeFile(
"c/d/foo.txt",
"""
foo
bar
baz
"""
.trimIndent()
)
tempDir
.resolve("PklProject")
.writeString(
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val packager =
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
packager.run()
val expectedMetadata = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0")
val expectedMetadataChecksum = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.sha256")
val expectedArchive = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.zip")
val expectedArchiveChecksum = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.zip.sha256")
assertThat(expectedMetadata).exists()
assertThat(expectedMetadata)
.hasContent(
"""
{
"name": "mypackage",
"packageUri": "package://example.com/mypackage@1.0.0",
"version": "1.0.0",
"packageZipUrl": "https://foo.com",
"packageZipChecksums": {
"sha256": "7f515fbc4b229ba171fac78c7c3f2c2e68e2afebae8cfba042b12733943a2813"
},
"dependencies": {},
"authors": []
}
"""
.trimIndent()
)
assertThat(expectedArchive).exists()
assertThat(expectedArchive.zipFilePaths())
.hasSameElementsAs(listOf("/", "/c", "/c/d", "/c/d/foo.txt", "/a", "/a/b", "/a/b/foo.pkl"))
assertThat(expectedMetadataChecksum)
.hasContent("203ef387f217a3caee7f19819ef2b50926929269241cb7b3a29d95237b2c7f4b")
assertThat(expectedArchiveChecksum)
.hasContent("7f515fbc4b229ba171fac78c7c3f2c2e68e2afebae8cfba042b12733943a2813")
FileSystems.newFileSystem(URI("jar:" + expectedArchive.toUri()), mutableMapOf<String, String>())
.use { fs ->
assertThat(fs.getPath("a/b/foo.pkl")).hasSameTextualContentAs(fooPkl)
assertThat(fs.getPath("c/d/foo.txt")).hasSameTextualContentAs(fooTxt)
}
}
@Test
fun `generate package with excludes`(@TempDir tempDir: Path) {
tempDir.apply {
writeEmptyFile("a/b/c/d.bin")
writeEmptyFile("input/foo/bar.txt")
writeEmptyFile("z.bin")
writeEmptyFile("main.pkl")
writeEmptyFile("main.test.pkl")
writeEmptyFile("child/main.pkl")
writeEmptyFile("child/main.test.pkl")
}
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
exclude {
"*.bin"
"child/main.pkl"
"*.test.pkl"
}
}
"""
.trimIndent()
)
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
val expectedArchive = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.zip")
assertThat(expectedArchive.zipFilePaths())
.hasSameElementsAs(
listOf(
"/",
"/a",
"/a/b",
"/a/b/c",
"/child",
"/input",
"/input/foo",
"/input/foo/bar.txt",
"/main.pkl",
)
)
}
@Test
fun `generate packages with local dependencies`(@TempDir tempDir: Path) {
val projectDir = tempDir.resolve("project")
val project2Dir = tempDir.resolve("project2")
projectDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
["project2"] = import("../project2/PklProject")
}
"""
.trimIndent()
)
projectDir.writeFile(
"PklProject.deps.json",
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/project2@5": {
"type": "local",
"uri": "projectpackage://localhost:12110/project2@5.0.0",
"path": "../project2"
}
}
}
"""
.trimIndent()
)
project2Dir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "project2"
baseUri = "package://localhost:12110/project2"
version = "5.0.0"
packageZipUrl = "https://foo.com/project2.zip"
}
"""
.trimIndent()
)
project2Dir.writeFile(
"PklProject.deps.json",
"""
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
"""
.trimIndent()
)
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(projectDir, project2Dir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
val expectedMetadata = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0")
assertThat(expectedMetadata).exists()
assertThat(expectedMetadata)
.hasContent(
"""
{
"name": "mypackage",
"packageUri": "package://example.com/mypackage@1.0.0",
"version": "1.0.0",
"packageZipUrl": "https://foo.com",
"packageZipChecksums": {
"sha256": "7d08a65078e0bfc382c16fe1bb94546ab9a11e6f551087f362a4515ca98102fc"
},
"dependencies": {
"birds": {
"uri": "package://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"project2": {
"uri": "package://localhost:12110/project2@5.0.0",
"checksums": {
"sha256": "ddebb806e5b218ebb1a2baa14ad206b46e7a0c1585fa9863a486c75592bc8b16"
}
}
},
"authors": []
}
"""
.trimIndent()
)
val project2Metadata = tempDir.resolve(".out/project2@5.0.0/project2@5.0.0")
assertThat(project2Metadata).exists()
assertThat(project2Metadata.readString())
.isEqualTo(
"""
{
"name": "project2",
"packageUri": "package://localhost:12110/project2@5.0.0",
"version": "5.0.0",
"packageZipUrl": "https://foo.com/project2.zip",
"packageZipChecksums": {
"sha256": "7d08a65078e0bfc382c16fe1bb94546ab9a11e6f551087f362a4515ca98102fc"
},
"dependencies": {},
"authors": []
}
"""
.trimIndent()
)
}
@Test
fun `generate package with local dependencies fails if local dep is not included for packaging`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("project")
val project2Dir = tempDir.resolve("project2")
projectDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
["project2"] = import("../project2/PklProject")
}
"""
.trimIndent()
)
projectDir.writeFile(
"PklProject.deps.json",
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/project2@5": {
"type": "local",
"uri": "projectpackage://localhost:12110/project2@5.0.0",
"path": "../project2"
}
}
}
"""
.trimIndent()
)
project2Dir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "project2"
baseUri = "package://localhost:12110/project2"
version = "5.0.0"
packageZipUrl = "https://foo.com/project2.zip"
}
"""
.trimIndent()
)
project2Dir.writeFile(
"PklProject.deps.json",
"""
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
"""
.trimIndent()
)
assertThatCode {
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(projectDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
}
.hasMessageContaining("which is not included for packaging")
}
@Test
fun `import path verification -- relative path outside project dir`(@TempDir tempDir: Path) {
tempDir.writeFile(
"main.pkl",
"""
import "../foo.pkl"
res = foo
"""
.trimIndent()
)
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val e =
assertThrows<CliException> {
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
}
assertThat(e.message)
.startsWith(
"""
Pkl Error
Path `../foo.pkl` includes path segments that are outside the project root directory.
1 | import "../foo.pkl"
^^^^^^^^^^^^
"""
.trimIndent()
)
}
@Test
fun `import path verification -- absolute import from root dir`(@TempDir tempDir: Path) {
tempDir.writeFile(
"main.pkl",
"""
import "$tempDir/foo.pkl"
res = foo
"""
.trimIndent()
)
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val e =
assertThrows<CliException> {
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
}
assertThat(e.message)
.startsWith(
"""
Pkl Error
Path `$tempDir/foo.pkl` includes path segments that are outside the project root directory.
"""
.trimIndent()
)
}
@Test
fun `import path verification -- absolute read from root dir`(@TempDir tempDir: Path) {
tempDir.writeFile(
"main.pkl",
"""
res = read("$tempDir/foo.pkl")
"""
.trimIndent()
)
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val e =
assertThrows<CliException> {
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
}
assertThat(e.message)
.startsWith(
"""
Pkl Error
Path `$tempDir/foo.pkl` includes path segments that are outside the project root directory.
"""
.trimIndent()
)
}
@Test
fun `import path verification -- passing`(@TempDir tempDir: Path) {
tempDir.writeFile(
"foo/bar.pkl",
"""
import "baz.pkl"
"""
.trimIndent()
)
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
package {
name = "mypackage"
version = "1.0.0"
baseUri = "package://example.com/mypackage"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = StringWriter()
)
.run()
}
@Test
fun `multiple projects`(@TempDir tempDir: Path) {
tempDir.writeFile("project1/main.pkl", "res = 1")
tempDir.writeFile(
"project1/PklProject",
"""
amends "pkl:Project"
package {
name = "project1"
version = "1.0.0"
baseUri = "package://example.com/project1"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
tempDir.writeFile("project2/main2.pkl", "res = 2")
tempDir.writeFile(
"project2/PklProject",
"""
amends "pkl:Project"
package {
name = "project2"
version = "2.0.0"
baseUri = "package://example.com/project2"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val out = StringWriter()
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir.resolve("project1"), tempDir.resolve("project2")),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = true,
consoleWriter = out
)
.run()
assertThat(out.toString())
.isEqualTo(
"""
.out/project1@1.0.0/project1@1.0.0.zip
.out/project1@1.0.0/project1@1.0.0.zip.sha256
.out/project1@1.0.0/project1@1.0.0
.out/project1@1.0.0/project1@1.0.0.sha256
.out/project2@2.0.0/project2@2.0.0.zip
.out/project2@2.0.0/project2@2.0.0.zip.sha256
.out/project2@2.0.0/project2@2.0.0
.out/project2@2.0.0/project2@2.0.0.sha256
"""
.trimIndent()
)
assertThat(tempDir.resolve(".out/project1@1.0.0/project1@1.0.0.zip").zipFilePaths())
.hasSameElementsAs(listOf("/", "/main.pkl"))
assertThat(tempDir.resolve(".out/project2@2.0.0/project2@2.0.0.zip").zipFilePaths())
.hasSameElementsAs(listOf("/", "/main2.pkl"))
}
@Test
fun `publish checks`(@TempDir tempDir: Path) {
PackageServer.ensureStarted()
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
tempDir.writeFile("project/main.pkl", "res = 1")
tempDir.writeFile(
"project/PklProject",
// intentionally conflict with localhost:12110/birds@0.5.0 from our test fixtures
"""
amends "pkl:Project"
package {
name = "birds"
version = "0.5.0"
baseUri = "package://localhost:12110/birds"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val e =
assertThrows<CliException> {
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir.resolve("project")),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = false,
consoleWriter = StringWriter()
)
.run()
}
assertThat(e)
.hasMessageStartingWith(
"""
Package `package://localhost:12110/birds@0.5.0` was already published with different contents.
Computed checksum: 7324e17214b6dcda63ebfb57d5a29b077af785c13bed0dc22b5138628a3f8d8f
Published checksum: 3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118
"""
.trimIndent()
)
}
@Test
fun `publish check when package is not yet published`(@TempDir tempDir: Path) {
PackageServer.ensureStarted()
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
tempDir.writeFile("project/main.pkl", "res = 1")
tempDir.writeFile(
"project/PklProject",
"""
amends "pkl:Project"
package {
name = "mangos"
version = "1.0.0"
baseUri = "package://localhost:12110/mangos"
packageZipUrl = "https://foo.com"
}
"""
.trimIndent()
)
val out = StringWriter()
CliProjectPackager(
CliBaseOptions(workingDir = tempDir),
listOf(tempDir.resolve("project")),
CliTestOptions(),
".out/%{name}@%{version}",
skipPublishCheck = false,
consoleWriter = out
)
.run()
assertThat(out.toString())
.isEqualTo(
"""
.out/mangos@1.0.0/mangos@1.0.0.zip
.out/mangos@1.0.0/mangos@1.0.0.zip.sha256
.out/mangos@1.0.0/mangos@1.0.0
.out/mangos@1.0.0/mangos@1.0.0.sha256
"""
.trimIndent()
)
}
private fun Path.zipFilePaths(): List<String> {
return FileSystems.newFileSystem(URI("jar:${toUri()}"), emptyMap<String, String>()).use { fs ->
Files.walk(fs.getPath("/")).map { it.toString() }.collect(Collectors.toList())
}
}
}

View File

@@ -0,0 +1,439 @@
/**
* 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.cli
import java.io.StringWriter
import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
class CliProjectResolverTest {
companion object {
@BeforeAll
@JvmStatic
fun beforeAll() {
PackageServer.ensureStarted()
}
}
@Test
fun `missing PklProject when inferring a project dir`(@TempDir tempDir: Path) {
val packager =
CliProjectResolver(
CliBaseOptions(workingDir = tempDir),
emptyList(),
consoleWriter = StringWriter(),
errWriter = StringWriter()
)
val err = assertThrows<CliException> { packager.run() }
assertThat(err).hasMessageStartingWith("No project visible to the working directory.")
}
@Test
fun `missing PklProject when explict dir is provided`(@TempDir tempDir: Path) {
val packager =
CliProjectResolver(
CliBaseOptions(),
listOf(tempDir),
consoleWriter = StringWriter(),
errWriter = StringWriter()
)
val err = assertThrows<CliException> { packager.run() }
assertThat(err).hasMessageStartingWith("Directory $tempDir does not contain a PklProject file.")
}
@Test
fun `basic project`(@TempDir tempDir: Path) {
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
}
"""
.trimIndent()
)
CliProjectResolver(
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
listOf(tempDir),
consoleWriter = StringWriter(),
errWriter = StringWriter()
)
.run()
val expectedOutput = tempDir.resolve("PklProject.deps.json")
assertThat(expectedOutput)
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5"
}
}
}
}
"""
.trimIndent()
)
}
@Test
fun `basic project, inferred from working dir`(@TempDir tempDir: Path) {
tempDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
}
"""
.trimIndent()
)
CliProjectResolver(
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
),
emptyList(),
consoleWriter = StringWriter(),
errWriter = StringWriter()
)
.run()
val expectedOutput = tempDir.resolve("PklProject.deps.json")
assertThat(expectedOutput)
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5"
}
}
}
}
"""
.trimIndent()
)
}
@Test
fun `local dependencies`(@TempDir tempDir: Path) {
val projectDir = tempDir.resolve("theproject")
projectDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
["project2"] = import("../project2/PklProject")
}
"""
.trimIndent()
)
projectDir.writeFile(
"../project2/PklProject",
"""
amends "pkl:Project"
package {
name = "project2"
baseUri = "package://localhost:12110/package2"
version = "5.0.0"
packageZipUrl = "https://foo.com/package2.zip"
}
dependencies {
["fruit"] {
uri = "package://localhost:12110/fruit@1.0.5"
}
["project3"] = import("../project3/PklProject")
}
"""
.trimIndent()
)
projectDir.writeFile(
"../project3/PklProject",
"""
amends "pkl:Project"
package {
name = "project3"
baseUri = "package://localhost:12110/package3"
version = "5.0.0"
packageZipUrl = "https://foo.com/package3.zip"
}
dependencies {
["fruit"] {
uri = "package://localhost:12110/fruit@1.1.0"
}
}
"""
.trimIndent()
)
CliProjectResolver(
CliBaseOptions(caCertificates = listOf(FileTestUtils.selfSignedCertificate)),
listOf(projectDir),
consoleWriter = StringWriter(),
errWriter = StringWriter()
)
.run()
val expectedOutput = projectDir.resolve("PklProject.deps.json")
assertThat(expectedOutput)
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.1.0",
"checksums": {
"sha256": "98ad9fc407a79dc3fd5595e7a29c3803ade0a6957c18ec94b8a1624360b24f01"
}
},
"package://localhost:12110/package2@5": {
"type": "local",
"uri": "projectpackage://localhost:12110/package2@5.0.0",
"path": "../project2"
},
"package://localhost:12110/package3@5": {
"type": "local",
"uri": "projectpackage://localhost:12110/package3@5.0.0",
"path": "../project3"
}
}
}
"""
.trimIndent()
)
}
@Test
fun `local dependency overridden by remote dependency`(@TempDir tempDir: Path) {
val projectDir = tempDir.resolve("theproject")
projectDir.writeFile(
"PklProject",
"""
amends "pkl:Project"
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
["fruit"] = import("../fruit/PklProject")
}
"""
.trimIndent()
)
projectDir.writeFile(
"../fruit/PklProject",
"""
amends "pkl:Project"
package {
name = "fruit"
baseUri = "package://localhost:12110/fruit"
version = "1.0.0"
packageZipUrl = "https://foo.com/fruit.zip"
}
"""
.trimIndent()
)
val consoleOut = StringWriter()
val errOut = StringWriter()
CliProjectResolver(
CliBaseOptions(caCertificates = listOf(FileTestUtils.selfSignedCertificate)),
listOf(projectDir),
consoleWriter = consoleOut,
errWriter = errOut
)
.run()
val expectedOutput = projectDir.resolve("PklProject.deps.json")
assertThat(expectedOutput)
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5"
}
}
}
}
"""
.trimIndent()
)
assertThat(errOut.toString())
.isEqualTo(
"WARN: local dependency `package://localhost:12110/fruit@1.0.0` was overridden to remote dependency `package://localhost:12110/fruit@1.0.5`.\n"
)
}
@Test
fun `resolving multiple projects`(@TempDir tempDir: Path) {
tempDir.writeFile(
"project1/PklProject",
"""
amends "pkl:Project"
dependencies {
["birds"] {
uri = "package://localhost:12110/birds@0.5.0"
}
}
"""
.trimIndent()
)
tempDir.writeFile(
"project2/PklProject",
"""
amends "pkl:Project"
dependencies {
["fruit"] {
uri = "package://localhost:12110/fruit@1.1.0"
}
}
"""
.trimIndent()
)
val consoleOut = StringWriter()
val errOut = StringWriter()
CliProjectResolver(
CliBaseOptions(caCertificates = listOf(FileTestUtils.selfSignedCertificate)),
listOf(tempDir.resolve("project1"), tempDir.resolve("project2")),
consoleWriter = consoleOut,
errWriter = errOut
)
.run()
assertThat(consoleOut.toString())
.isEqualTo(
"""
$tempDir/project1/PklProject.deps.json
$tempDir/project2/PklProject.deps.json
"""
.trimIndent()
)
assertThat(tempDir.resolve("project1/PklProject.deps.json"))
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5"
}
}
}
}
"""
.trimIndent()
)
assertThat(tempDir.resolve("project2/PklProject.deps.json"))
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.1.0",
"checksums": {
"sha256": "98ad9fc407a79dc3fd5595e7a29c3803ade0a6957c18ec94b8a1624360b24f01"
}
}
}
}
"""
.trimIndent()
)
}
}

View File

@@ -0,0 +1,208 @@
/**
* 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.cli
import com.github.ajalt.clikt.core.MissingArgument
import com.github.ajalt.clikt.core.subcommands
import java.io.StringWriter
import java.net.URI
import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.cli.commands.EvalCommand
import org.pkl.cli.commands.RootCommand
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.commons.cli.CliTestOptions
import org.pkl.commons.readString
import org.pkl.commons.toUri
import org.pkl.commons.writeString
import org.pkl.core.Release
class CliTestRunnerTest {
@Test
fun `CliTestRunner succeed test`(@TempDir tempDir: Path) {
val code =
"""
amends "pkl:test"
facts {
["succeed"] {
8 == 8
3 == 3
}
}
"""
.trimIndent()
val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter()
val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings"))
val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
runner.run()
assertThat(out.toString().stripFileAndLines(tempDir))
.isEqualTo(
"""
module test
succeed ✅
"""
.trimIndent()
)
assertThat(err.toString()).isEqualTo("")
}
@Test
fun `CliTestRunner fail test`(@TempDir tempDir: Path) {
val code =
"""
amends "pkl:test"
facts {
["fail"] {
4 == 9
"foo" == "bar"
}
}
"""
.trimIndent()
val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter()
val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings"))
val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
assertThatCode { runner.run() }.hasMessage("Tests failed.")
assertThat(out.toString().stripFileAndLines(tempDir))
.isEqualTo(
"""
module test
fail ❌
4 == 9 ❌
"foo" == "bar" ❌
"""
.trimIndent()
)
assertThat(err.toString()).isEqualTo("")
}
@Test
fun `CliTestRunner JUnit reports`(@TempDir tempDir: Path) {
val code =
"""
amends "pkl:test"
facts {
["foo"] {
9 == trace(9)
"foo" == "foo"
}
["fail"] {
5 == 9
}
}
"""
.trimIndent()
val input = tempDir.resolve("test.pkl").writeString(code).toString()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings"))
val testOpts = CliTestOptions(junitDir = tempDir)
val runner = CliTestRunner(opts, testOpts)
assertThatCode { runner.run() }.hasMessageContaining("failed")
val junitReport = tempDir.resolve("test.xml").readString().stripFileAndLines(tempDir)
assertThat(junitReport)
.isEqualTo(
"""
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="test" tests="2" failures="1">
<testcase classname="test" name="foo"></testcase>
<testcase classname="test" name="fail">
<failure message="Fact Failure">5 == 9 ❌</failure>
</testcase>
<system-err><![CDATA[9 = 9
]]></system-err>
</testsuite>
"""
.trimIndent()
)
}
@Test
fun `CliTestRunner duplicated JUnit reports`(@TempDir tempDir: Path) {
val foo =
"""
module foo
amends "pkl:test"
facts {
["foo"] {
1 == 1
}
}
"""
.trimIndent()
val bar =
"""
module foo
amends "pkl:test"
facts {
["foo"] {
1 == 1
}
}
"""
.trimIndent()
val input = tempDir.resolve("test.pkl").writeString(foo).toString()
val input2 = tempDir.resolve("test.pkl").writeString(bar).toString()
val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri(), input2.toUri()),
settings = URI("pkl:settings")
)
val testOpts = CliTestOptions(junitDir = tempDir)
val runner = CliTestRunner(opts, testOpts)
assertThatCode { runner.run() }.hasMessageContaining("failed")
}
@Test
fun `no source modules specified has same message as pkl eval`() {
val e1 = assertThrows<CliException> { CliTestRunner(CliBaseOptions(), CliTestOptions()).run() }
val e2 =
assertThrows<MissingArgument> {
val rootCommand =
RootCommand("pkl", Release.current().versionInfo(), "").subcommands(EvalCommand(""))
rootCommand.parse(listOf("eval"))
}
assertThat(e1).hasMessageContaining("Missing argument \"<modules>\"")
assertThat(e1.message!!.replace("test", "eval")).isEqualTo(e2.helpMessage())
}
private fun String.stripFileAndLines(tmpDir: Path) =
replace(tmpDir.toUri().toString(), "/tempDir/").replace(Regex(""" \(.*, line \d+\)"""), "")
}

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.cli.repl
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.pkl.commons.toPath
import org.pkl.core.Loggers
import org.pkl.core.SecurityManagers
import org.pkl.core.StackFrameTransformers
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.repl.ReplRequest
import org.pkl.core.repl.ReplResponse
import org.pkl.core.repl.ReplServer
class ReplMessagesTest {
private val server =
ReplServer(
SecurityManagers.defaultManager,
Loggers.stdErr(),
listOf(ModuleKeyFactories.standardLibrary),
listOf(),
mapOf(),
mapOf(),
null,
null,
null,
"/".toPath(),
StackFrameTransformers.defaultTransformer
)
@Test
fun `run examples`() {
val examples = ReplMessages.examples
var startIndex = examples.indexOf("```")
while (startIndex != -1) {
val endIndex = examples.indexOf("```", startIndex + 3)
assertThat(endIndex).isNotEqualTo(-1)
val text =
examples
.substring(startIndex + 3, endIndex)
.lines()
.filterNot { it.contains(":force") }
.joinToString("\n")
val responses = server.handleRequest(ReplRequest.Eval("1", text, true, true))
assertThat(responses.size).isBetween(1, 9)
assertThat(responses).hasOnlyElementsOfType(ReplResponse.EvalSuccess::class.java)
startIndex = examples.indexOf("```", endIndex + 3)
}
}
}

View File

@@ -0,0 +1,30 @@
/**
* 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.cli
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import org.pkl.commons.createParentDirectories
import org.pkl.commons.writeString
fun Path.writeFile(fileName: String, contents: String): Path {
return resolve(fileName).apply {
createParentDirectories()
writeString(contents, StandardCharsets.UTF_8)
}
}
fun Path.writeEmptyFile(fileName: String): Path = writeFile(fileName, "")