mirror of
https://github.com/apple/pkl.git
synced 2026-05-25 16:19:20 +02:00
1290 lines
29 KiB
Kotlin
1290 lines
29 KiB
Kotlin
/*
|
|
* Copyright © 2025-2026 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.CliktError
|
|
import com.github.ajalt.clikt.core.MissingOption
|
|
import com.github.ajalt.clikt.core.PrintCompletionMessage
|
|
import com.github.tomakehurst.wiremock.junit5.WireMockTest
|
|
import java.io.ByteArrayOutputStream
|
|
import java.net.URI
|
|
import java.nio.charset.StandardCharsets
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
import kotlin.io.path.ExperimentalPathApi
|
|
import kotlin.io.path.createDirectories
|
|
import kotlin.io.path.createParentDirectories
|
|
import kotlin.io.path.deleteRecursively
|
|
import org.assertj.core.api.Assertions.assertThat
|
|
import org.junit.jupiter.api.AfterAll
|
|
import org.junit.jupiter.api.AfterEach
|
|
import org.junit.jupiter.api.Test
|
|
import org.junit.jupiter.api.assertThrows
|
|
import org.pkl.commons.cli.CliBaseOptions
|
|
import org.pkl.commons.cli.CliException
|
|
import org.pkl.commons.test.FileTestUtils
|
|
import org.pkl.commons.test.PackageServer
|
|
import org.pkl.commons.writeString
|
|
|
|
@OptIn(ExperimentalPathApi::class)
|
|
@WireMockTest(httpsEnabled = true, proxyMode = true)
|
|
class CliCommandRunnerTest {
|
|
private val renderOptions =
|
|
"""
|
|
extends "pkl:Command"
|
|
|
|
options: Options
|
|
|
|
output {
|
|
value = options
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
|
|
companion object {
|
|
private val packageServer = PackageServer()
|
|
|
|
@AfterAll
|
|
@JvmStatic
|
|
fun afterAll() {
|
|
packageServer.close()
|
|
}
|
|
}
|
|
|
|
// use manually constructed temp dir instead of @TempDir to work around
|
|
// https://forums.developer.apple.com/thread/118358
|
|
private val tempDir: Path = run {
|
|
val baseDir = FileTestUtils.rootProjectDir.resolve("pkl-cli/build/tmp/CliCommandRunnerTest")
|
|
baseDir.createDirectories()
|
|
Files.createTempDirectory(baseDir, null)
|
|
}
|
|
|
|
@AfterEach
|
|
fun afterEach() {
|
|
tempDir.deleteRecursively()
|
|
}
|
|
|
|
private fun writePklFile(fileName: String, contents: String): URI {
|
|
tempDir.resolve(fileName).createParentDirectories()
|
|
return tempDir.resolve(fileName).writeString(contents).toUri()
|
|
}
|
|
|
|
private fun runToStdout(options: CliBaseOptions, args: List<String>): String {
|
|
val outWriter = ByteArrayOutputStream()
|
|
CliCommandRunner(
|
|
options,
|
|
setOf("root-dir"),
|
|
emptySet(),
|
|
args,
|
|
outWriter,
|
|
ByteArrayOutputStream(),
|
|
)
|
|
.run()
|
|
return outWriter.toString(StandardCharsets.UTF_8)
|
|
}
|
|
|
|
@Test
|
|
fun `missing required flag`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
foo: String
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val exc =
|
|
assertThrows<MissingOption> {
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri), testMode = true), listOf())
|
|
}
|
|
assertThat(exc.paramName).isEqualTo("--foo")
|
|
}
|
|
|
|
@Test
|
|
fun `primitive flags`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
`number-as-int`: Number
|
|
`number-as-float`: Number
|
|
`number-nullable`: Number?
|
|
`number-default`: Number = 100.0
|
|
`number-default-overridden`: Number = 100.0
|
|
|
|
float: Float
|
|
`float-without-decimals`: Float
|
|
`float-nullable`: Float?
|
|
`float-default`: Float = 100.0
|
|
`float-default-overridden`: Float = 100.0
|
|
|
|
int: Int
|
|
`int-nullable`: Int?
|
|
`int-default`: Int = 100
|
|
`int-default-overridden`: Int = 100
|
|
|
|
int8: Int8
|
|
int16: Int16
|
|
int32: Int32
|
|
uint: UInt
|
|
uint8: UInt8
|
|
uint16: UInt16
|
|
uint32: UInt32
|
|
|
|
boolean: Boolean
|
|
`boolean-nullable`: Boolean?
|
|
`boolean-default`: Boolean = true
|
|
`boolean-default-overridden`: Boolean = false
|
|
|
|
string: String
|
|
`string-nullable`: String?
|
|
`string-default`: String = "default"
|
|
`string-default-overridden`: String = "default"
|
|
|
|
char: Char
|
|
`char-nullable`: Char?
|
|
`char-default`: Char = "a"
|
|
`char-default-overridden`: Char = "b"
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf(
|
|
"--number-as-int=123",
|
|
"--number-as-float=123.0",
|
|
"--number-default-overridden=-200.0",
|
|
"--float=123.456",
|
|
"--float-without-decimals=789",
|
|
"--float-default-overridden=-200",
|
|
"--int=123",
|
|
"--int-default-overridden=-200",
|
|
"--int8=127",
|
|
"--int16=32767",
|
|
"--int32=2147483647",
|
|
"--uint=0",
|
|
"--uint8=255",
|
|
"--uint16=65535",
|
|
"--uint32=4294967295",
|
|
"--boolean=n",
|
|
"--boolean-default-overridden=1",
|
|
"--string=foobar",
|
|
"--string-default-overridden=non-default",
|
|
"--char=X",
|
|
"--char-default-overridden=c",
|
|
),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
`number-as-int` = 123
|
|
`number-as-float` = 123.0
|
|
`number-nullable` = null
|
|
`number-default` = 100.0
|
|
`number-default-overridden` = -200.0
|
|
float = 123.456
|
|
`float-without-decimals` = 789.0
|
|
`float-nullable` = null
|
|
`float-default` = 100.0
|
|
`float-default-overridden` = -200.0
|
|
int = 123
|
|
`int-nullable` = null
|
|
`int-default` = 100
|
|
`int-default-overridden` = -200
|
|
int8 = 127
|
|
int16 = 32767
|
|
int32 = 2147483647
|
|
uint = 0
|
|
uint8 = 255
|
|
uint16 = 65535
|
|
uint32 = 4294967295
|
|
boolean = false
|
|
`boolean-nullable` = null
|
|
`boolean-default` = true
|
|
`boolean-default-overridden` = true
|
|
string = "foobar"
|
|
`string-nullable` = null
|
|
`string-default` = "default"
|
|
`string-default-overridden` = "non-default"
|
|
char = "X"
|
|
`char-nullable` = null
|
|
`char-default` = "a"
|
|
`char-default-overridden` = "c"
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `primitive arguments`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
`number-as-int`: Number
|
|
@Argument
|
|
`number-as-float`: Number
|
|
|
|
@Argument
|
|
float: Float
|
|
@Argument
|
|
`float-without-decimals`: Float
|
|
|
|
@Argument
|
|
int: Int
|
|
|
|
@Argument
|
|
int8: Int8
|
|
@Argument
|
|
int16: Int16
|
|
@Argument
|
|
int32: Int32
|
|
@Argument
|
|
uint: UInt
|
|
@Argument
|
|
uint8: UInt8
|
|
@Argument
|
|
uint16: UInt16
|
|
@Argument
|
|
uint32: UInt32
|
|
|
|
@Argument
|
|
boolean: Boolean
|
|
|
|
@Argument
|
|
string: String
|
|
|
|
@Argument
|
|
char: Char
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf(
|
|
"123",
|
|
"123.0",
|
|
"123.456",
|
|
"789",
|
|
"123",
|
|
"127",
|
|
"32767",
|
|
"2147483647",
|
|
"0",
|
|
"255",
|
|
"65535",
|
|
"4294967295",
|
|
"n",
|
|
"foobar",
|
|
"X",
|
|
),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
`number-as-int` = 123
|
|
`number-as-float` = 123.0
|
|
float = 123.456
|
|
`float-without-decimals` = 789.0
|
|
int = 123
|
|
int8 = 127
|
|
int16 = 32767
|
|
int32 = 2147483647
|
|
uint = 0
|
|
uint8 = 255
|
|
uint16 = 65535
|
|
uint32 = 4294967295
|
|
boolean = false
|
|
string = "foobar"
|
|
char = "X"
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `enum flags`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
typealias MyEnum = "d" | "e" | *"f"
|
|
class Options {
|
|
enum: "a" | "b" | "c"
|
|
`enum-default`: "a" | *"b" | "c"
|
|
`enum-explicit-default`: "a" | "b" | "c" = "c"
|
|
`enum-alias-default`: MyEnum
|
|
`enum-alias-explicit-default`: MyEnum = "e"
|
|
`enum-alias-default-overridden`: MyEnum
|
|
|
|
`enum-single`: "x"
|
|
`enum-single-nullable`: "x"?
|
|
`enum-single-explicit-default`: "x" = "x"
|
|
`enum-single-overridden`: "x"
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("--enum=a", "--enum-alias-default-overridden=d", "--enum-single-overridden=x"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
enum = "a"
|
|
`enum-default` = "b"
|
|
`enum-explicit-default` = "c"
|
|
`enum-alias-default` = "f"
|
|
`enum-alias-explicit-default` = "e"
|
|
`enum-alias-default-overridden` = "d"
|
|
`enum-single` = "x"
|
|
`enum-single-nullable` = null
|
|
`enum-single-explicit-default` = "x"
|
|
`enum-single-overridden` = "x"
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `enum args`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
typealias MyEnum = "d" | "e" | *"f"
|
|
class Options {
|
|
@Argument
|
|
enum: "a" | "b" | "c"
|
|
@Argument
|
|
`enum-default`: "a" | *"b" | "c"
|
|
@Argument
|
|
`enum-alias-default`: MyEnum
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("a", "c", "d"))
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
enum = "a"
|
|
`enum-default` = "c"
|
|
`enum-alias-default` = "d"
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `collection flags`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
list: List<Number>
|
|
`list-nullable`: List<Number>?
|
|
`list-default`: List<Number> = List(1, 2, 300.0)
|
|
set: Set<Number>
|
|
`set-nullable`: Set<Number>?
|
|
`set-default`: Set<Number> = Set(1, 2, 300.0, 2)
|
|
|
|
`enum-list`: List<"a" | "b" | *"c">
|
|
`enum-set`: Set<"a" | "b" | *"c">
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf(
|
|
"--list=1",
|
|
"--list=0",
|
|
"--list=0.0",
|
|
"--list=1",
|
|
"--set=1",
|
|
"--set=0",
|
|
"--set=0.0",
|
|
"--set=1",
|
|
"--enum-list=a",
|
|
"--enum-list=a",
|
|
"--enum-list=b",
|
|
"--enum-set=a",
|
|
"--enum-set=a",
|
|
"--enum-set=b",
|
|
),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
list = List(1, 0, 0.0, 1)
|
|
`list-nullable` = null
|
|
`list-default` = List(1, 2, 300.0)
|
|
set = Set(1, 0, 0.0)
|
|
`set-nullable` = null
|
|
`set-default` = Set(1, 2, 300.0)
|
|
`enum-list` = List("a", "a", "b")
|
|
`enum-set` = Set("a", "b")
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `sequence args`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
list: List<Number>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("1", "0", "0.0", "1"))
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
list = List(1, 0, 0.0, 1)
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
|
|
val moduleUri2 =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
set: Set<Number>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output2 =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri2)), listOf("1", "0", "0.0", "1"))
|
|
assertThat(output2)
|
|
.isEqualTo(
|
|
"""
|
|
set = Set(1, 0, 0.0)
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
|
|
val moduleUri3 =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
listing: Listing<Number>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output3 =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri3)), listOf("1", "0", "0.0", "1"))
|
|
assertThat(output3)
|
|
.isEqualTo(
|
|
"""
|
|
listing {
|
|
1
|
|
0
|
|
0.0
|
|
1
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `keyval args`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
map: Map<Number, Number>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("1=0", "0.0=1"))
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
map = Map(1, 0, 0.0, 1)
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
|
|
val moduleUri2 =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
mapping: Mapping<Number, Number>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output2 =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri2)), listOf("1=0", "0.0=1"))
|
|
assertThat(output2)
|
|
.isEqualTo(
|
|
"""
|
|
mapping {
|
|
[1] = 0
|
|
[0.0] = 1
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
|
|
val moduleUri3 =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument
|
|
pair: Pair<Number, Number>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output3 = runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri3)), listOf("1=0.0"))
|
|
assertThat(output3)
|
|
.isEqualTo(
|
|
"""
|
|
pair = Pair(1, 0.0)
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `map flags`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
typealias MyEnum = "a" | "b" | *"c"
|
|
class Options {
|
|
map: Map<Char, Number>
|
|
`map-nullable`: Map<Char, Number>?
|
|
`map-default`: Map<Char, Number> = Map("x", 123, "y", 456.789)
|
|
|
|
`enum-map`: Map<MyEnum, MyEnum>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("--map=a=0.0", "--map=b=1", "--enum-map=a=b", "--enum-map=b=c"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
map = Map("a", 0.0, "b", 1)
|
|
`map-nullable` = null
|
|
`map-default` = Map("x", 123, "y", 456.789)
|
|
`enum-map` = Map("a", "b", "b", "c")
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `mapping flags`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
typealias MyEnum = "a" | "b" | *"c"
|
|
class Options {
|
|
mapping: Mapping<Char, Number>
|
|
`mapping-nullable`: Mapping<Char, Number>?
|
|
`mapping-default`: Mapping<Char, Number> = new { ["x"] = 123; ["y"] = 456.789 }
|
|
|
|
`enum-mapping`: Mapping<MyEnum, MyEnum>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("--mapping=a=0.0", "--mapping=b=1", "--enum-mapping=a=b", "--enum-mapping=b=c"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
mapping {
|
|
["a"] = 0.0
|
|
["b"] = 1
|
|
}
|
|
`mapping-nullable` = null
|
|
`mapping-default` {
|
|
["x"] = 123
|
|
["y"] = 456.789
|
|
}
|
|
`enum-mapping` {
|
|
["a"] = "b"
|
|
["b"] = "c"
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `pair flags`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
typealias MyEnum = "a" | "b" | *"c"
|
|
class Options {
|
|
pair: Pair<Char, Number>
|
|
`pair-nullable`: Pair<Char, Number>?
|
|
`pair-default`: Pair<Char, Number> = Pair("x", 123)
|
|
|
|
`enum-pair`: Pair<MyEnum, MyEnum>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("--pair=a=0.0", "--enum-pair=a=b"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
pair = Pair("a", 0.0)
|
|
`pair-nullable` = null
|
|
`pair-default` = Pair("x", 123)
|
|
`enum-pair` = Pair("a", "b")
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `convert Duration`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument { convert = module.convertDuration }
|
|
a: Duration
|
|
@Argument { convert = module.convertDuration }
|
|
b: Duration
|
|
@Argument { convert = module.convertDuration }
|
|
c: Duration
|
|
@Argument { convert = module.convertDuration }
|
|
d: Duration
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("10.h", "10H", "10.5.MS", "10.5d"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
a = 10.h
|
|
b = 10.h
|
|
c = 10.5.ms
|
|
d = 10.5.d
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `convert DataSize`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument { convert = module.convertDataSize }
|
|
a: DataSize
|
|
@Argument { convert = module.convertDataSize }
|
|
b: DataSize
|
|
@Argument { convert = module.convertDataSize }
|
|
c: DataSize
|
|
@Argument { convert = module.convertDataSize }
|
|
d: DataSize
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("10.gb", "10GB", "10.5.MB", "10.5tib"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
a = 10.gb
|
|
b = 10.gb
|
|
c = 10.5.mb
|
|
d = 10.5.tib
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `convert import`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
"""
|
|
extends "pkl:Command"
|
|
|
|
options: Options
|
|
|
|
output {
|
|
value = (options) {
|
|
fromImport {
|
|
baz = true // assert that imported modules are not forced
|
|
}
|
|
}
|
|
}
|
|
|
|
class Options {
|
|
@Argument { convert = (it) -> new Import{ uri = it } }
|
|
fromImport: Module
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val importUri =
|
|
writePklFile(
|
|
"import.pkl",
|
|
"""
|
|
foo = 1
|
|
bar = "baz"
|
|
baz: Boolean
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val output =
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf(importUri.toString()))
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
fromImport {
|
|
foo = 1
|
|
bar = "baz"
|
|
baz = true
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `transformAll import`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
"""
|
|
extends "pkl:Command"
|
|
|
|
options: Options
|
|
|
|
output {
|
|
value = (options) {
|
|
fromImport {
|
|
baz = true // assert that imported modules are not forced
|
|
}
|
|
}
|
|
}
|
|
|
|
class Options {
|
|
@Flag {
|
|
convert = (it) -> new Import{ uri = it }
|
|
transformAll = (values) -> values.firstOrNull ?? new Import { uri = "./default.pkl" }
|
|
}
|
|
fromImport: Module
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val importUri =
|
|
writePklFile(
|
|
"default.pkl",
|
|
"""
|
|
foo = 1
|
|
bar = "baz"
|
|
baz: Boolean
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir),
|
|
emptyList(),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
fromImport {
|
|
foo = 1
|
|
bar = "baz"
|
|
baz = true
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `convert glob import`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
"""
|
|
extends "pkl:Command"
|
|
import "base.pkl"
|
|
|
|
options: Options
|
|
|
|
output {
|
|
value = (options) {
|
|
fromGlobImport {
|
|
[[true]] {
|
|
baz = true // assert that imported modules are not forced
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class Options {
|
|
@Argument { convert = (it) -> new Import { uri = it; glob = true }; multiple = false }
|
|
fromGlobImport: Mapping<String, base>
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val baseImport =
|
|
writePklFile(
|
|
"base.pkl",
|
|
"""
|
|
foo: Int
|
|
bar: String
|
|
baz: Boolean
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
writePklFile(
|
|
"glob1.pkl",
|
|
"""
|
|
amends "base.pkl"
|
|
foo = 1
|
|
bar = "baz"
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
writePklFile(
|
|
"glob2.pkl",
|
|
"""
|
|
amends "base.pkl"
|
|
foo = 2
|
|
bar = "qux"
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val importDirUri = baseImport.resolve(".")
|
|
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf(importDirUri.resolve("./glob*.pkl").toString()),
|
|
)
|
|
assertThat(output.replace(importDirUri.toString(), "file:/<dir>/"))
|
|
.isEqualTo(
|
|
"""
|
|
fromGlobImport {
|
|
["file:/<dir>/glob1.pkl"] {
|
|
foo = 1
|
|
bar = "baz"
|
|
baz = true
|
|
}
|
|
["file:/<dir>/glob2.pkl"] {
|
|
foo = 2
|
|
bar = "qux"
|
|
baz = true
|
|
}
|
|
}
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `convert that throws`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument { convert = (it) -> throw("oops!") }
|
|
foo: String
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val exc =
|
|
assertThrows<CliktError> {
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("hi"))
|
|
}
|
|
assertThat(exc.message).contains("oops!")
|
|
}
|
|
|
|
@Test
|
|
fun `convert with eval error`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Argument { convert = (it) -> it.noSuchMethod() }
|
|
foo: String
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val exc =
|
|
assertThrows<CliktError> {
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("hi"))
|
|
}
|
|
assertThat(exc.message).contains("Cannot find method `noSuchMethod` in class `String`.")
|
|
}
|
|
|
|
@Test
|
|
fun `convert with stack overflow`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
const function overflow(it) = overflow(it)
|
|
class Options {
|
|
@Argument { convert = (it) -> overflow(it) }
|
|
foo: String
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val exc =
|
|
assertThrows<CliktError> {
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("hi"))
|
|
}
|
|
assertThat(exc.message).contains("A stack overflow occurred.")
|
|
}
|
|
|
|
@Test
|
|
fun `boolean flag`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@BooleanFlag
|
|
`bool-true`: Boolean
|
|
@BooleanFlag
|
|
`bool-false`: Boolean
|
|
@BooleanFlag
|
|
`bool-nullable`: Boolean?
|
|
@BooleanFlag
|
|
`bool-default-true`: Boolean = true
|
|
@BooleanFlag
|
|
`bool-default-false`: Boolean = false
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("--bool-true", "--no-bool-false"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
`bool-true` = true
|
|
`bool-false` = false
|
|
`bool-nullable` = null
|
|
`bool-default-true` = true
|
|
`bool-default-false` = false
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `boolean flag with bad type`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@BooleanFlag
|
|
foo: String
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val exc =
|
|
assertThrows<CliException> {
|
|
runToStdout(CliBaseOptions(sourceModules = listOf(moduleUri)), listOf("hi"))
|
|
}
|
|
assertThat(exc.message)
|
|
.contains("Option `foo` with annotation `@BooleanFlag` has invalid type `String`.")
|
|
}
|
|
|
|
@Test
|
|
fun `counted flag`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@CountedFlag { shortName = "a" }
|
|
int: Int
|
|
@CountedFlag { shortName = "b" }
|
|
int8: Int8
|
|
@CountedFlag { shortName = "c" }
|
|
int16: Int16
|
|
@CountedFlag { shortName = "d" }
|
|
int32: Int32
|
|
@CountedFlag { shortName = "x" }
|
|
uint: UInt
|
|
@CountedFlag { shortName = "y" }
|
|
uint8: UInt8
|
|
@CountedFlag { shortName = "g" }
|
|
uint16: UInt16
|
|
@CountedFlag { shortName = "i" }
|
|
uint32: UInt32
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("-abbcccddddxxxxxyyyyyygggggggiiiiiiii"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
int = 1
|
|
int8 = 2
|
|
int16 = 3
|
|
int32 = 4
|
|
uint = 5
|
|
uint8 = 6
|
|
uint16 = 7
|
|
uint32 = 8
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `test transformAll`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
@Flag {
|
|
multiple = true
|
|
transformAll = (values) -> values.fold(0, (res, acc) -> res + acc)
|
|
}
|
|
foo: Int
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val output =
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("--foo=1", "--foo=5", "--foo=8"),
|
|
)
|
|
assertThat(output)
|
|
.isEqualTo(
|
|
"""
|
|
foo = 14
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `completion candidates`() {
|
|
val moduleUri =
|
|
writePklFile(
|
|
"cmd.pkl",
|
|
renderOptions +
|
|
"""
|
|
class Options {
|
|
none: String?
|
|
enum: *"a" | "b" | "c"
|
|
@Flag { completionCandidates = "paths" }
|
|
path: String?
|
|
@Flag { completionCandidates { "foo"; "bar"; "baz" } }
|
|
explicit: String?
|
|
@Argument
|
|
enumArg: *"a" | "b" | "c"
|
|
@Argument { completionCandidates = "paths" }
|
|
pathArg: String
|
|
@Argument { completionCandidates { "foo"; "bar"; "baz" } }
|
|
explicitArg: String
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val exc =
|
|
assertThrows<PrintCompletionMessage> {
|
|
runToStdout(
|
|
CliBaseOptions(sourceModules = listOf(moduleUri)),
|
|
listOf("a", "foo", "bar", "shell-completion", "bash"),
|
|
)
|
|
}
|
|
assertThat(exc.message)
|
|
.contains(
|
|
"""
|
|
"--none")
|
|
;;
|
|
"--enum")
|
|
COMPREPLY=($(compgen -W 'a b c' -- "${'$'}{word}"))
|
|
;;
|
|
"--path")
|
|
__complete_files "${'$'}{word}"
|
|
;;
|
|
"--explicit")
|
|
COMPREPLY=($(compgen -W 'bar baz foo' -- "${'$'}{word}"))
|
|
;;
|
|
"--help")
|
|
;;
|
|
"enumArg")
|
|
COMPREPLY=($(compgen -W '' -- "${'$'}{word}"))
|
|
;;
|
|
"pathArg")
|
|
__complete_files "${'$'}{word}"
|
|
;;
|
|
"explicitArg")
|
|
COMPREPLY=($(compgen -W 'bar baz foo' -- "${'$'}{word}"))
|
|
;;"""
|
|
)
|
|
}
|
|
}
|