mirror of
https://github.com/apple/pkl.git
synced 2026-03-18 15:23:58 +01:00
Initial commit
This commit is contained in:
35
pkl-cli/src/main/java/org/pkl/cli/svm/InitFeature.java
Normal file
35
pkl-cli/src/main/java/org/pkl/cli/svm/InitFeature.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
236
pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt
Normal file
236
pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt
Normal file
85
pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
90
pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt
Normal file
90
pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
53
pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectResolver.kt
Normal file
53
pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectResolver.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
72
pkl-cli/src/main/kotlin/org/pkl/cli/CliRepl.kt
Normal file
72
pkl-cli/src/main/kotlin/org/pkl/cli/CliRepl.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
33
pkl-cli/src/main/kotlin/org/pkl/cli/CliServer.kt
Normal file
33
pkl-cli/src/main/kotlin/org/pkl/cli/CliServer.kt
Normal 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!!)
|
||||
}
|
||||
}
|
||||
104
pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt
Normal file
104
pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt
Normal file
55
pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
88
pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt
Normal file
88
pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
149
pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt
Normal file
149
pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
pkl-cli/src/main/kotlin/org/pkl/cli/commands/ReplCommand.kt
Normal file
36
pkl-cli/src/main/kotlin/org/pkl/cli/commands/ReplCommand.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
39
pkl-cli/src/main/kotlin/org/pkl/cli/commands/RootCommand.kt
Normal file
39
pkl-cli/src/main/kotlin/org/pkl/cli/commands/RootCommand.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
46
pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt
Normal file
46
pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
219
pkl-cli/src/main/kotlin/org/pkl/cli/repl/Repl.kt
Normal file
219
pkl-cli/src/main/kotlin/org/pkl/cli/repl/Repl.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
45
pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCommands.kt
Normal file
45
pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCommands.kt
Normal 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()
|
||||
}
|
||||
182
pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCompleters.kt
Normal file
182
pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCompleters.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
104
pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplMessages.kt
Normal file
104
pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplMessages.kt
Normal 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)
|
||||
}
|
||||
34
pkl-cli/src/main/kotlin/org/pkl/cli/repl/package-info.java
Normal file
34
pkl-cli/src/main/kotlin/org/pkl/cli/repl/package-info.java
Normal 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;
|
||||
7
pkl-cli/src/test/files/projects/project1/PklProject
vendored
Normal file
7
pkl-cli/src/test/files/projects/project1/PklProject
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
amends "pkl:Project"
|
||||
|
||||
dependencies {
|
||||
["birds"] {
|
||||
uri = "package://localhost:12110/birds@0.5.0"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
1217
pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt
Normal file
1217
pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt
Normal file
File diff suppressed because it is too large
Load Diff
123
pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt
Normal file
123
pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt
Normal 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
|
||||
}
|
||||
}
|
||||
959
pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt
Normal file
959
pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
439
pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectResolverTest.kt
Normal file
439
pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectResolverTest.kt
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
208
pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt
Normal file
208
pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt
Normal 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+\)"""), "")
|
||||
}
|
||||
64
pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt
Normal file
64
pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
pkl-cli/src/test/kotlin/org/pkl/cli/testExtensions.kt
Normal file
30
pkl-cli/src/test/kotlin/org/pkl/cli/testExtensions.kt
Normal 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, "")
|
||||
Reference in New Issue
Block a user