Files
pkl/pkl-cli/src/test/kotlin/org/pkl/cli/CliCommandRunnerTest.kt
T

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