1 Commits

Author SHA1 Message Date
darwincereska
4714a11eef feat(update): large update with new types, fixed bugs 2025-11-18 23:22:34 -05:00
19 changed files with 1648 additions and 119 deletions

View File

@@ -4,7 +4,7 @@ plugins {
}
group = "org.kargs"
version = "1.0.0"
version = "1.0.2"
repositories {
mavenCentral()
@@ -13,7 +13,27 @@ repositories {
dependencies {
testImplementation(kotlin("test"))
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlin:kotlin-reflect:2.2.21")
implementation(kotlin("reflect"))
// JUnit 5 testing dependencies
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0")
testImplementation(kotlin("test"))
// For assertions
testImplementation("org.assertj:assertj-core:3.24.2")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = true
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
mavenPublishing {

View File

@@ -1 +1 @@
rootProject.name = "karg"
rootProject.name = "kargs"

View File

@@ -1,23 +1,94 @@
package org.kargs
/**
* Sealed class representing different argument types with conversion and validation logic
*/
sealed class ArgType<T>(val typeName: String) {
/**
* Convert a string value to the target type
* @throws ArgumentParseException if conversion fails
*/
abstract fun convert(value: String): T
/**
* Validate that a value is acceptable for this type
* @return true if valid, false otherwise
*/
open fun validate(value: T): Boolean = true
/**
* Get a description of valid values for this type
*/
open fun getValidationDescription(): String = "any $typeName"
object StringType : ArgType<kotlin.String>("String") {
override fun convert(value: String) = value
}
object IntType : ArgType<kotlin.Int>("Int") {
override fun convert(value: String) = value.toInt()
override fun convert(value: String): kotlin.Int = value.toIntOrNull() ?: throw ArgumentParseException("`$value` is not a valid integer")
}
object BooleanType : ArgType<kotlin.Boolean>("Boolean") {
override fun convert(value: String) = value.toBoolean()
override fun convert(value: String): kotlin.Boolean {
return when (value.lowercase()) {
"true", "yes", "1", "on" -> true
"false", "no", "0", "off" -> false
else -> throw ArgumentParseException("'$value' is not a valid boolean (true/false, yes/no, 1/0, on/off)")
}
}
}
object DoubleType : ArgType<kotlin.Double>("Double") {
override fun convert(value: String): kotlin.Double {
return value.toDoubleOrNull()
?: throw ArgumentParseException("'$value' is not a valid number")
}
}
/**
* Create a constrained integer type with min/max bounds
*/
class IntRange(private val min: kotlin.Int, private val max: kotlin.Int) : ArgType<kotlin.Int>("Int") {
override fun convert(value: String): kotlin.Int {
val intValue = value.toIntOrNull() ?: throw ArgumentParseException("`$value` is not a valid integer")
if (intValue !in min..max) {
throw ArgumentParseException("`$value` must be between $min and $max")
}
return intValue
}
override fun getValidationDescription(): String = "integer between $min and $max"
}
/**
* Create an enum type from list of valid choices
*/
class Choice(private val choices: List<kotlin.String>) : ArgType<kotlin.String>("Choice") {
override fun convert(value: String): kotlin.String {
if (value !in choices) {
throw ArgumentParseException("`$value` is not a valid choice. Valid options: ${choices.joinToString(", ")}")
}
return value
}
override fun getValidationDescription(): String = "one of: ${choices.joinToString(", ")}"
}
companion object {
val String: ArgType<kotlin.String> = StringType
val Int: ArgType<kotlin.Int> = IntType
val Boolean: ArgType<kotlin.Boolean> = BooleanType
val Double: ArgType<kotlin.Double> = DoubleType
/**
* Create an integer type with bounds
*/
fun intRange(min: kotlin.Int, max: kotlin.Int) = IntRange(min, max)
/**
* Create a choice type from valid options
*/
fun choice(vararg options: kotlin.String) = Choice(options.toList())
}
}

View File

@@ -1,26 +1,40 @@
package org.kargs
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Represents a positional command-line argument
*
* @param type The type converter for this argument's value
* @param name The name of this argument (used in help)
* @param description Help text for this argument
* @param required Whether this argument must be provided
*/
class Argument<T>(
val type: ArgType<T>,
val name: String,
val description: String = "",
description: String? = null,
val required: Boolean = true
) : ReadWriteProperty<Any?, T?> {
) : KargsProperty<T>(description) {
init {
require(name.isNotBlank()) { "Argument name cannot be blank" }
}
private var _value: T? = null
override fun parseValue(str: String) {
value = type.convert(str)
}
var value: T?
get() = _value
set(v) { _value = v }
override fun isValid(): Boolean {
return if (required) {
value != null && type.validate(value!!)
} else {
value?.let { type.validate(it) } ?: true
}
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = _value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { _value = value }
fun parseValue(input: String) {
_value = type.convert(input)
override fun getValidationError(): String? {
return when {
required && value == null -> "Argument '$name' is required"
value != null && !type.validate(value!!) -> "Invalid value for argument '$name': expected ${type.getValidationDescription()}"
else -> null
}
}
}

View File

@@ -1,24 +1,53 @@
package org.kargs
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Represents a boolean flag that doesn't take a value (e.g. --verbose, -v)
*
* @param longName The long form name (used with --)
* @param shortName The short form name (used with -)
* @param description Help text for this flag
* @param defaultValue Default value (typically false)
*/
class Flag(
val longName: String,
val shortName: String? = null,
val description: String = "",
val default: Boolean = false
) : ReadWriteProperty<Any?, Boolean> {
description: String? = null,
private val defaultValue: Boolean = false,
) : KargsProperty<Boolean>(description) {
private var _value = default
private var wasExplicitlySet = false
var value: Boolean
get() = _value
set(v) { _value = v }
init {
value = defaultValue
override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean = _value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { _value = value }
// Validate names
require(longName.isNotBlank()) { "Long name cannot be blank" }
require(!longName.startsWith("-")) { "Long name should not start with dashes" }
shortName?.let {
require(it.length == 1) { "Short name must be exactly one character" }
require(!it.startsWith("-")) { "Short name should not start with dashes" }
}
}
fun setFlag() { _value = true }
override fun parseValue(str: String) {
value = when (str.lowercase()) {
"true", "yes", "1", "on" -> true
"false", "no", "0", "off" -> false
else -> throw ArgumentParseException("Invalid flag value: $str")
}
wasExplicitlySet = true
}
/**
* Set the flag to true (called when flag is present)
*/
fun setFlag() {
value = true
wasExplicitlySet = true
}
/**
* Check if this flag was explicitly set
*/
fun isSet(): Boolean = wasExplicitlySet
}

View File

@@ -0,0 +1,51 @@
package org.kargs
import kotlin.reflect.KProperty
/**
* Base class for all command-line argument properties (options, flags, arguments)
*/
abstract class KargsProperty<T>(val description: String? = null) {
var value: T? = null
protected set
internal lateinit var parent: Subcommand
/**
* Parse a string value into the appropiate type
* @throws ArgumentParseException if parsing fails
*/
abstract fun parseValue(str: String)
/**
* Validate the current value
* @return true if valid, false otherwise
*/
open fun isValid(): Boolean = true
/**
* Get validation error message if value is invalid
*/
open fun getValidationError(): String? = null
/**
* Property delagate setup - registers this property with its parent subcommand
*/
operator fun provideDelegate(thisRef: Subcommand, prop: KProperty<*>): KargsProperty<T> {
parent = thisRef
parent.registerProperty(this)
return this
}
/**
* Property delagate getter
*/
operator fun getValue(thisRef: Subcommand, property: KProperty<*>): T? = value
/*
* Property delagate setter
*/
operator fun setValue(thisRef: Subcommand, property: KProperty<*>, value: T?) {
this.value = value
}
}

View File

@@ -1,28 +1,67 @@
package org.kargs
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Represents a command-line option that takes a value (e.g. --output file.txt)
*
* @param type The type converter for this options's value
* @param longName The long form name (used with --)
* @param shortName The short form name (used with -)
* @param description Help text for this option
* @param required Whether this option must be provided
* @param defaultValue Default value if not provided
*/
class Option<T>(
val type: ArgType<T>,
val longName: String,
val shortName: String? = null,
val description: String = "",
description: String? = null,
val required: Boolean = false,
val default: T? = null
) : ReadWriteProperty<Any?, T?> {
private val defaultValue: T? = null
) : KargsProperty<T>(description) {
private var _value: T? = default
private var wasExplicitlySet = false
var value: T?
get() = _value
set(v) { _value = v }
init {
// Set the default value if provided
defaultValue?.let { value = it }
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = _value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { _value = value }
fun parseValue(input: String) {
_value = type.convert(input)
// Validate names
require(longName.isNotBlank()) { "Long name cannot be blank" }
require(!longName.startsWith("-")) { "Long name should not start with dashes" }
shortName?.let {
require(it.length == 1) { "Short name must be exactly one character" }
require(!it.startsWith("-")) { "Short name should not start with dashes" }
}
}
}
override fun parseValue(str: String) {
value = type.convert(str)
wasExplicitlySet = true
}
override fun isValid(): Boolean {
return if (required) {
value != null && type.validate(value!!)
} else {
value?.let { type.validate(it) } ?: true
}
}
override fun getValidationError(): String? {
return when {
required && value == null -> "Option --$longName is required"
value != null && !type.validate(value!!) -> "Invalid value for --$longName: expected ${type.getValidationDescription()}"
else -> null
}
}
/**
* Check if the option has been set (different from default)
*/
fun isSet(): Boolean = wasExplicitlySet
/**
* Get the value or default
*/
fun getValueOrDefault(): T? = value ?: defaultValue
}

View File

@@ -1,98 +1,292 @@
package org.kargs
class Parser(val programName: String) {
/**
* Main argument parser that handles command-line argument parsing and routing to subcommands.
*
* @param programName The name of the program (used in help messages)
* @param config Configuration options for the parser behavior
*/
class Parser(
val programName: String,
private val config: ParserConfig = ParserConfig.DEFAULT
) {
private val commands = mutableListOf<Subcommand>()
/**
* Register one or more subcommands with this parser
*/
fun subcommands(vararg cmds: Subcommand) {
commands.addAll(cmds)
}
/**
* Parse the provided command-line arguments and execute the appropiate command
*
* @param args Array of command-line arguments
*/
fun parse(args: Array<String>) {
if (args.isEmpty()) {
if (config.helpOnEmpty) {
printGlobalHelp()
}
return
}
// Check for global help
if (args.contains("--help") || args.contains("-h")) {
printGlobalHelp()
return
}
val cmdName = args[0]
val cmd = commands.firstOrNull { it.name == cmdName || it.aliases.contains(cmdName) }
val cmd = findCommand(cmdName)
if (cmd == null) {
println("Unknown command: $cmdName")
printGlobalHelp()
printError("Unknown command: $cmdName")
if (!config.strictMode) {
printGlobalHelp()
}
return
}
// Check for command specific help
if (args.contains("--help") || args.contains("-h")) {
cmd.printHelp()
return
}
// Reflection: find all Option, Flag, Argument properties
val props = cmd::class.members.filterIsInstance<kotlin.reflect.KProperty<*>>()
val options = props.mapNotNull { it.getter.call(cmd) as? Option<*> }
val flags = props.mapNotNull { it.getter.call(cmd) as? Flag }
val arguments = props.mapNotNull { it.getter.call(cmd) as? Argument<*> }
try {
parseCommandArgs(cmd, args.sliceArray(1 until args.size))
validateRequiredOptions(cmd)
cmd.execute()
} catch (e: ArgumentParseException) {
printError(e.message ?: "Parse error")
cmd.printHelp()
throw e
}
}
/**
* Find a command by name or alias
*/
private fun findCommand(name: String): Subcommand? {
val searchName = if (config.caseSensitive) name else name.lowercase()
return commands.firstOrNull { cmd ->
val cmdName = if (config.caseSensitive) cmd.name else cmd.name.lowercase()
val aliases = if (config.caseSensitive) cmd.aliases else cmd.aliases.map { it.lowercase() }
cmdName == searchName || aliases.contains(searchName)
}
}
/**
* Parse arguments for a specific command
*/
private fun parseCommandArgs(cmd: Subcommand, args: Array<String>) {
var i = 0
val positionalArgs = mutableListOf<String>()
// Parse args starting at index 1
var i = 1
while (i < args.size) {
val arg = args[i]
when {
arg.startsWith("--") -> {
val key = arg.removePrefix("--")
val option = options.firstOrNull { it.longName == key }
val flag = flags.firstOrNull { it.longName == key }
when {
option != null -> {
i++
if (i >= args.size) throw IllegalArgumentException("Missing value for option --$key")
option.parseValue(args[i])
}
flag != null -> flag.setFlag()
else -> println("Unknown option --$key")
}
i = parseLongOption(cmd, key, args, i)
}
arg.startsWith("-") -> {
arg.startsWith("-") && arg.length > 1 -> {
val key = arg.removePrefix("-")
val option = options.firstOrNull { it.shortName == key }
val flag = flags.firstOrNull { it.shortName == key }
when {
option != null -> {
i++
if (i >= args.size) throw IllegalArgumentException("Missing value for option -$key")
option.parseValue(args[i])
}
flag != null -> flag.setFlag()
else -> println("Unknown option -$key")
}
i = parseShortOption(cmd, key, args, i)
}
else -> {
// Positional arguments
val nextArg = arguments.firstOrNull { it.value == null }
if (nextArg != null) {
nextArg.parseValue(arg)
} else {
println("Unexpected argument: $arg")
}
positionalArgs.add(arg)
}
}
i++
}
// Execute the command
cmd.execute()
// Handle positional arguments
parsePositionalArguments(cmd, positionalArgs)
}
private fun printGlobalHelp() {
println("Usage: $programName <command> [options]")
println("\nCommands:")
commands.forEach {
println(" ${it.name}\t${it.description}")
/**
* Parse a long option (--option)
*/
private fun parseLongOption(cmd: Subcommand, key: String, args: Array<String>, index: Int): Int {
val option = cmd.options.firstOrNull { it.longName == key }
val flag = cmd.flags.firstOrNull { it.longName == key }
return when {
option != null -> {
if (index + 1 >= args.size) {
throw ArgumentParseException("Missing value for option --$key")
}
try {
option.parseValue(args[index + 1])
index + 1
} catch (e: Exception) {
throw ArgumentParseException("Invalid value for option --$key: ${e.message}")
}
}
flag != null -> {
flag.setFlag()
index
}
else -> {
if (config.strictMode) {
throw ArgumentParseException("Unknown option --$key")
} else {
printWarning("Unknown option --$key")
index
}
}
}
}
/**
* Parse a short option (-o)
*/
private fun parseShortOption(cmd: Subcommand, key: String, args: Array<String>, index: Int): Int {
// Handle combined short flags like -acc
if (key.length > 1) {
key.forEach { char ->
val flag = cmd.flags.firstOrNull { it.shortName == char.toString() }
if (flag != null) {
flag.setFlag()
} else if (config.strictMode) {
throw ArgumentParseException("Unknown flag -$char")
}
}
return index
}
val option = cmd.options.firstOrNull { it.shortName == key }
val flag = cmd.flags.firstOrNull { it.shortName == key }
return when {
option != null -> {
if (index + 1 >= args.size) {
throw ArgumentParseException("Missing value for option -$key")
}
try {
option.parseValue(args[index + 1])
index + 1
} catch (e: Exception) {
throw ArgumentParseException("Invalid value for option -$key: ${e.message}")
}
}
flag != null -> {
flag.setFlag()
index
}
else -> {
if (config.strictMode) {
throw ArgumentParseException("Unknown option -$key")
} else {
printWarning("Unknown option -$key")
index
}
}
}
}
/**
* Parse positional arguments
*/
private fun parsePositionalArguments(cmd: Subcommand, args: List<String>) {
val arguments = cmd.arguments
if (args.size > arguments.size) {
val extra = args.drop(arguments.size)
if (config.strictMode) {
throw ArgumentParseException("Too many arguments: ${extra.joinToString(", ")}")
} else {
printWarning("Ignoring extra arguments: ${extra.joinToString(", ")}")
}
}
args.forEachIndexed { index, value ->
if (index < arguments.size) {
try {
arguments[index].parseValue(value)
} catch (e: Exception) {
throw ArgumentParseException("Invalid value for argument ${arguments[index].name}: ${e.message}")
}
}
}
}
/**
* Validate that all required options have been provided
*/
private fun validateRequiredOptions(cmd: Subcommand) {
val missingRequired = cmd.options.filter { it.required && it.value == null }
if (missingRequired.isNotEmpty()) {
val missing = missingRequired.joinToString(", ") { "--${it.longName}" }
throw ArgumentParseException("Missing required options: $missing")
}
}
/**
* Print global help menu
*/
private fun printGlobalHelp() {
println(colorize("Usage: $programName <command> [options]", Color.BOLD))
println()
println(colorize("Commands:", Color.BOLD))
commands.forEach { cmd ->
val aliases = if (cmd.aliases.isNotEmpty()) " (${cmd.aliases.joinToString(", ")})" else ""
println(" ${colorize(cmd.name, Color.GREEN)}$aliases")
if (cmd.description.isNotEmpty()) {
println(" ${cmd.description}")
}
}
println()
println("Use `$programName <command> --help` for more information about a command.")
}
/**
* Print error message with optional coloring
*/
private fun printError(message: String) {
println(colorize("Error: $message", Color.RED))
}
/**
* Print warning message with optional coloring
*/
private fun printWarning(message: String) {
println(colorize("Warning: $message", Color.YELLOW))
}
/**
* Apply color to text if colors are enabled
*/
private fun colorize(text: String, color: Color): String {
return if (config.colorsEnabled) {
"${color.code}$text${Color.RESET.code}"
} else {
text
}
}
/**
* ANSI color codes for terminal output
*/
private enum class Color(val code: String) {
RESET("\u001B[0m"),
RED("\u001B[31m"),
GREEN("\u001B[32m"),
YELLOW("\u001B[33m"),
BOLD("\u001B[1m")
}
}
/**
* Custom exception for argument parsing errors
*/
class ArgumentParseException(message: String) : Exception(message)

View File

@@ -0,0 +1,17 @@
package org.kargs
/**
* Configuration class for customizing parser behavior
*/
data class ParserConfig(
val colorsEnabled: Boolean = true,
val strictMode: Boolean = false, // Whether to fail on unknown options
val helpOnEmpty: Boolean = true, // Show help when no args provided
val caseSensitive: Boolean = true,
val allowAbbreviations: Boolean = false, // Allow partial option matching
val programVersion: String? = null // Adds --version and -v flag if set
) {
companion object {
val DEFAULT = ParserConfig()
}
}

View File

@@ -1,41 +1,110 @@
package org.kargs
/**
* Base class for defining subcommands with their options, flags, and arguments
*
* @param name The name for this subcommand
* @param description Help description for this subcommand
* @param aliases Alternative names for this subcommand
*/
abstract class Subcommand(
val name: String,
val description: String = "",
val aliases: List<String> = emptyList()
) {
private val _options = mutableListOf<Option<*>>()
private val _flags = mutableListOf<Flag>()
private val _arguments = mutableListOf<Argument<*>>()
init {
require(name.isNotBlank()) { "Subcommand name cannot be blank" }
}
/**
* Register a property (option, flag, argument) with this subcommand
* Called automatically by property delagates
*/
fun registerProperty(prop: KargsProperty<*>) {
when (prop) {
is Option<*> -> _options += prop
is Flag -> _flags += prop
is Argument<*> -> _arguments += prop
}
}
// Public read-only access to registered properties
val options: List<Option<*>> get() = _options
val flags: List<Flag> get() = _flags
val arguments: List<Argument<*>> get() = _arguments
/**
* Execute this subcommand - must be implemented by subclasses
*/
abstract fun execute()
open fun printHelp() {
println("Usage: $name [options] [arguments]")
if (description.isNotEmpty()) println(description)
val props = this::class.members.filterIsInstance<kotlin.reflect.KProperty<*>>()
val options = props.mapNotNull { it.getter.call(this) as? Option<*> }
val flags = props.mapNotNull { it.getter.call(this) as? Flag }
val arguments = props.mapNotNull { it.getter.call(this) as? Argument<*> }
/**
* Print help information for this subcommand
*/
fun printHelp() {
println("Usage: $name [options]${if (arguments.isNotEmpty()) " ${arguments.joinToString(" ") { "<${it.name}>" }}" else ""}")
if (description.isNotEmpty()) {
println()
println(description)
}
if (options.isNotEmpty()) {
println("\nOptions:")
options.forEach { o ->
println(" ${o.shortName?.let { "-$it, " } ?: ""}--${o.longName}\t${o.description}")
println()
println("Options:")
options.forEach { option ->
val shortName = option.shortName?.let { "-$it, " } ?: " "
val required = if (option.required) " (required)" else ""
val defaultVal = option.getValueOrDefault()?.let { " [default: $it]" } ?: ""
println(" $shortName--${option.longName}")
option.description?.let { desc ->
println(" $desc$required$defaultVal")
}
}
}
if (flags.isNotEmpty()) {
println("\nFlags:")
flags.forEach { f ->
println(" ${f.shortName?.let { "-$it, " } ?: ""}--${f.longName}\t${f.description}")
println()
println("Flags:")
flags.forEach { flag ->
val shortName = flag.shortName?.let { "-$it, " } ?: " "
println(" $shortName--${flag.longName}")
flag.description?.let { desc ->
println(" $desc")
}
}
}
if (arguments.isNotEmpty()) {
println("\nArguments:")
arguments.forEach { a ->
println(" ${a.name}\t${a.description}")
println()
println("Arguments:")
arguments.forEach { arg ->
val required = if (arg.required) " (required)" else " (optional)"
println(" ${arg.name}$required")
arg.description?.let { desc ->
println(" $desc")
}
}
}
}
}
/**
* Validate all properties in this subcommand
* @return list of validation errors, empty if all valid
*/
fun validate(): List<String> {
val errors = mutableListOf<String>()
(options + flags + arguments).forEach { prop ->
prop.getValidationError()?.let { error ->
errors.add(error)
}
}
return errors
}
}

View File

@@ -0,0 +1,107 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.kargs.ArgType
import org.kargs.ArgumentParseException
import kotlin.test.assertEquals
class ArgTypeTest {
@Test
fun `String type converts correctly`() {
val type = ArgType.String
assertEquals("hello", type.convert("hello"))
assertEquals("", type.convert(""))
assertEquals("123", type.convert("123"))
}
@Test
fun `Int type converts valid integers`() {
val type = ArgType.Int
assertEquals(42, type.convert("42"))
assertEquals(-10, type.convert("-10"))
assertEquals(0, type.convert("0"))
}
@Test
fun `Int type throws on invalid input`() {
val type = ArgType.Int
assertThrows<ArgumentParseException> { type.convert("not-a-number") }
assertThrows<ArgumentParseException> { type.convert("12.5") }
assertThrows<ArgumentParseException> { type.convert("") }
}
@ParameterizedTest
@ValueSource(strings = ["true", "TRUE", "yes", "YES", "1", "on", "ON"])
fun `Boolean type converts true values`(input: String) {
val type = ArgType.Boolean
assertEquals(true, type.convert(input))
}
@ParameterizedTest
@ValueSource(strings = ["false", "FALSE", "no", "NO", "0", "off", "OFF"])
fun `Boolean type converts false values`(input: String) {
val type = ArgType.Boolean
assertEquals(false, type.convert(input))
}
@Test
fun `Boolean type throws on invalid input`() {
val type = ArgType.Boolean
assertThrows<ArgumentParseException> { type.convert("maybe") }
assertThrows<ArgumentParseException> { type.convert("2") }
}
@Test
fun `Double type converts correctly`() {
val type = ArgType.Double
assertEquals(3.14, type.convert("3.14"))
assertEquals(-2.5, type.convert("-2.5"))
assertEquals(42.0, type.convert("42"))
}
@Test
fun `Double type throws on invalid input`() {
val type = ArgType.Double
assertThrows<ArgumentParseException> { type.convert("not-a-number") }
}
@Test
fun `IntRange validates bounds`() {
val type = ArgType.intRange(1, 10)
assertEquals(5, type.convert("5"))
assertEquals(1, type.convert("1"))
assertEquals(10, type.convert("10"))
assertThrows<ArgumentParseException> { type.convert("0") }
assertThrows<ArgumentParseException> { type.convert("11") }
assertThrows<ArgumentParseException> { type.convert("-5") }
}
@Test
fun `Choice validates options`() {
val type = ArgType.choice("red", "green", "blue")
assertEquals("red", type.convert("red"))
assertEquals("green", type.convert("green"))
assertEquals("blue", type.convert("blue"))
assertThrows<ArgumentParseException> { type.convert("yellow") }
assertThrows<ArgumentParseException> { type.convert("RED") } // case sensitive
}
@Test
fun `IntRange provides correct validation description`() {
val type = ArgType.intRange(5, 15)
assertEquals("integer between 5 and 15", type.getValidationDescription())
}
@Test
fun `Choice provides correct validation description`() {
val type = ArgType.choice("apple", "banana", "cherry")
assertEquals("one of: apple, banana, cherry", type.getValidationDescription())
}
}

View File

@@ -0,0 +1,78 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.kargs.Argument
import org.kargs.ArgType
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import kotlin.test.assertNull
class ArgumentTest {
@Test
fun `argument creation`() {
val arg = Argument(ArgType.String, "input", "Input file")
assertEquals("input", arg.name)
assertEquals("Input file", arg.description)
assertTrue(arg.required)
}
@Test
fun `optional argument`() {
val arg = Argument(ArgType.String, "output", "Output file", required = false)
assertFalse(arg.required)
assertTrue(arg.isValid()) // optional and null is valid
}
@Test
fun `argument parsing`() {
val arg = Argument(ArgType.Int, "count", "Item count")
assertNull(arg.value)
arg.parseValue("42")
assertEquals(42, arg.value)
}
@Test
fun `required argument validation`() {
val arg = Argument(ArgType.String, "required", "Required arg", required = true)
assertFalse(arg.isValid())
assertEquals("Argument 'required' is required", arg.getValidationError())
arg.parseValue("value")
assertTrue(arg.isValid())
assertNull(arg.getValidationError())
}
@Test
fun `argument throws on blank name`() {
assertThrows<IllegalArgumentException> {
Argument(ArgType.String, "", "Description")
}
assertThrows<IllegalArgumentException> {
Argument(ArgType.String, " ", "Description") // whitespace only
}
}
@Test
fun `argument with choice type`() {
val arg = Argument(ArgType.choice("red", "green", "blue"), "color", "Color choice")
arg.parseValue("red")
assertEquals("red", arg.value)
assertTrue(arg.isValid())
}
@Test
fun `argument with range type`() {
val arg = Argument(ArgType.intRange(1, 100), "percentage", "Percentage value")
arg.parseValue("50")
assertEquals(50, arg.value)
assertTrue(arg.isValid())
}
}

View File

@@ -0,0 +1,103 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.kargs.ArgumentParseException
import org.kargs.Flag
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
class FlagTest {
@Test
fun `flag creation with default false`() {
val flag = Flag("verbose", "v", "Verbose output")
assertEquals("verbose", flag.longName)
assertEquals("v", flag.shortName)
assertEquals("Verbose output", flag.description)
assertEquals(false, flag.value)
assertFalse(flag.isSet())
}
@Test
fun `flag creation with custom default`() {
val flag = Flag("enabled", defaultValue = true)
assertEquals(true, flag.value)
assertFalse(flag.isSet()) // not explicitly set, just default
}
@Test
fun `setFlag updates value`() {
val flag = Flag("debug", "d")
assertEquals(false, flag.value)
flag.setFlag()
assertEquals(true, flag.value)
assertTrue(flag.isSet())
}
@Test
fun `parseValue handles boolean strings`() {
val flag = Flag("test")
flag.parseValue("true")
assertEquals(true, flag.value)
flag.parseValue("false")
assertEquals(false, flag.value)
flag.parseValue("yes")
assertEquals(true, flag.value)
flag.parseValue("no")
assertEquals(false, flag.value)
flag.parseValue("1")
assertEquals(true, flag.value)
flag.parseValue("0")
assertEquals(false, flag.value)
}
@Test
fun `parseValue throws on invalid input`() {
val flag = Flag("test")
assertThrows<ArgumentParseException> {
flag.parseValue("maybe")
}
assertThrows<ArgumentParseException> {
flag.parseValue("2")
}
}
@Test
fun `flag throws on invalid names`() {
assertThrows<IllegalArgumentException> {
Flag("", "v") // blank long name
}
assertThrows<IllegalArgumentException> {
Flag("--invalid") // long name with dashes
}
assertThrows<IllegalArgumentException> {
Flag("valid", "ab") // short name too long
}
assertThrows<IllegalArgumentException> {
Flag("valid", "-v") // short name with dash
}
}
@Test
fun `flag isSet works correctly with defaults`() {
val flag = Flag("test", defaultValue = true)
assertFalse(flag.isSet()) // has default but not explicitly set
flag.setFlag()
assertTrue(flag.isSet()) // now explicitly set
}
}

View File

@@ -0,0 +1,128 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.kargs.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
class IntegrationTest {
class ComplexCommand : Subcommand("complex", "Complex test command", listOf("c")) {
val input by Argument(ArgType.String, "input", "Input file")
val output by Argument(ArgType.String, "output", "Output file", required = false)
val format by Option(ArgType.choice("json", "xml", "yaml"), "format", "f", "Output format", defaultValue = "json")
val threads by Option(ArgType.intRange(1, 32), "threads", "t", "Thread count", defaultValue = 4)
val timeout by Option(ArgType.Double, "timeout", description = "Timeout in seconds")
val config by Option(ArgType.String, "config", "c", "Config file", required = true)
val verbose by Flag("verbose", "v", "Verbose output")
val dryRun by Flag("dry-run", "n", "Dry run mode")
val force by Flag("force", "f", "Force operation")
var executed = false
override fun execute() {
executed = true
}
}
@Test
fun `complex command with all features`() {
val parser = Parser("myapp")
val cmd = ComplexCommand()
parser.subcommands(cmd)
parser.parse(arrayOf(
"complex",
"--config", "/path/to/config.yml",
"--format", "xml",
"--threads", "8",
"--timeout", "30.5",
"--verbose",
"--dry-run",
"input.txt",
"output.xml"
))
assertTrue(cmd.executed)
assertEquals("input.txt", cmd.input)
assertEquals("output.xml", cmd.output)
assertEquals("/path/to/config.yml", cmd.config)
assertEquals("xml", cmd.format)
assertEquals(8, cmd.threads)
assertEquals(30.5, cmd.timeout)
assertEquals(true, cmd.verbose)
assertEquals(true, cmd.dryRun)
assertEquals(false, cmd.force)
}
@Test
fun `complex command with short options and combined flags`() {
val parser = Parser("myapp")
val cmd = ComplexCommand()
parser.subcommands(cmd)
parser.parse(arrayOf(
"c", // using alias
"-c", "/config.yml",
"-f", "yaml",
"-t", "16",
"-vnf", // combined flags: verbose, dry-run (n), force
"input.txt"
))
assertTrue(cmd.executed)
assertEquals("input.txt", cmd.input)
assertEquals("/config.yml", cmd.config)
assertEquals("yaml", cmd.format)
assertEquals(16, cmd.threads)
assertEquals(true, cmd.verbose)
assertEquals(true, cmd.dryRun)
assertEquals(true, cmd.force)
}
@Test
fun `multiple commands in same parser`() {
val parser = Parser("tool")
val buildCmd = TestUtils.TestSubcommand("build", "Build project")
val testCmd = TestUtils.TestSubcommand("test", "Run tests")
val deployCmd = TestUtils.TestSubcommand("deploy", "Deploy project")
parser.subcommands(buildCmd, testCmd, deployCmd)
// Test build command
parser.parse(arrayOf("build", "--required", "value", "src/"))
assertTrue(buildCmd.executed)
// Reset and test different command
buildCmd.executed = false
parser.parse(arrayOf("test", "--required", "value", "tests/"))
assertTrue(testCmd.executed)
assertFalse(buildCmd.executed) // should not be executed again
}
@Test
fun `validation works end-to-end`() {
val parser = Parser("app")
val cmd = ComplexCommand()
parser.subcommands(cmd)
// Should validate all constraints
parser.parse(arrayOf(
"complex",
"--config", "config.yml",
"--format", "json", // valid choice
"--threads", "8", // valid range
"--timeout", "15.5", // valid double
"input.txt"
))
assertTrue(cmd.executed)
assertEquals("json", cmd.format)
assertEquals(8, cmd.threads)
assertEquals(15.5, cmd.timeout)
}
}

View File

@@ -0,0 +1,95 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.kargs.ArgType
import org.kargs.Option
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import kotlin.test.assertNull
class OptionTest {
@Test
fun `option creation with valid parameters`() {
val option = Option(ArgType.String, "output", "o", "Output file", required = true)
assertEquals("output", option.longName)
assertEquals("o", option.shortName)
assertEquals("Output file", option.description)
assertTrue(option.required)
}
@Test
fun `option with default value`() {
val option = Option(ArgType.Int, "threads", "t", "Thread count", defaultValue = 4)
assertEquals(4, option.value)
assertEquals(4, option.getValueOrDefault())
assertFalse(option.isSet())
}
@Test
fun `option parsing updates value`() {
val option = Option(ArgType.String, "name", "n")
assertNull(option.value)
option.parseValue("test")
assertEquals("test", option.value)
assertTrue(option.isSet())
}
@Test
fun `option validation for required field`() {
val option = Option(ArgType.String, "required", required = true)
assertFalse(option.isValid())
assertEquals("Option --required is required", option.getValidationError())
option.parseValue("value")
assertTrue(option.isValid())
assertNull(option.getValidationError())
}
@Test
fun `option throws on invalid names`() {
assertThrows<IllegalArgumentException> {
Option(ArgType.String, "", "o") // blank long name
}
assertThrows<IllegalArgumentException> {
Option(ArgType.String, "--invalid", "o") // long name with dashes
}
assertThrows<IllegalArgumentException> {
Option(ArgType.String, "valid", "ab") // short name too long
}
assertThrows<IllegalArgumentException> {
Option(ArgType.String, "valid", "-o") // short name with dash
}
}
@Test
fun `option with range type validation`() {
val option = Option(ArgType.intRange(1, 10), "count", "c", "Item count")
option.parseValue("5")
assertEquals(5, option.value)
assertTrue(option.isValid())
// Test that the option itself doesn't validate the range (ArgType does)
// The validation happens during parsing
}
@Test
fun `option isSet works correctly with defaults`() {
val option = Option(ArgType.String, "mode", defaultValue = "default")
assertFalse(option.isSet()) // has default but not explicitly set
option.parseValue("custom")
assertTrue(option.isSet()) // now explicitly set
option.parseValue("default") // set to same as default
assertTrue(option.isSet()) // still considered "set"
}
}

View File

@@ -0,0 +1,107 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.kargs.ArgumentParseException
import org.kargs.Parser
import org.kargs.ParserConfig
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
class ParserConfigTest {
@Test
fun `default config values`() {
val config = ParserConfig.DEFAULT
assertTrue(config.colorsEnabled)
assertFalse(config.strictMode)
assertTrue(config.helpOnEmpty)
assertTrue(config.caseSensitive)
assertFalse(config.allowAbbreviations)
}
@Test
fun `custom config values`() {
val config = ParserConfig(
colorsEnabled = false,
strictMode = true,
helpOnEmpty = false,
caseSensitive = false,
allowAbbreviations = true
)
assertFalse(config.colorsEnabled)
assertTrue(config.strictMode)
assertFalse(config.helpOnEmpty)
assertFalse(config.caseSensitive)
assertTrue(config.allowAbbreviations)
}
@Test
fun `strict mode affects unknown options`() {
val strictParser = Parser("test", ParserConfig(strictMode = true))
val lenientParser = Parser("test", ParserConfig(strictMode = false))
val testCmd = TestUtils.TestSubcommand()
strictParser.subcommands(testCmd)
lenientParser.subcommands(testCmd)
// Strict mode should throw
assertThrows<ArgumentParseException> {
strictParser.parse(arrayOf("test", "--unknown", "value", "--required", "req", "input.txt"))
}
// Lenient mode should warn but continue
val output = TestUtils.captureOutput {
lenientParser.parse(arrayOf("test", "--unknown", "value", "--required", "req", "input.txt"))
}
assertTrue(output.contains("Warning"))
assertTrue(testCmd.executed)
}
@Test
fun `helpOnEmpty config affects empty args behavior`() {
val helpParser = Parser("test", ParserConfig(helpOnEmpty = true))
val noHelpParser = Parser("test", ParserConfig(helpOnEmpty = false))
val testCmd = TestUtils.TestSubcommand()
helpParser.subcommands(testCmd)
noHelpParser.subcommands(testCmd)
// With helpOnEmpty = true, should show help
val helpOutput = TestUtils.captureOutput {
helpParser.parse(arrayOf())
}
assertTrue(helpOutput.contains("Usage:"))
// With helpOnEmpty = false, should do nothing
val noHelpOutput = TestUtils.captureOutput {
noHelpParser.parse(arrayOf())
}
assertEquals("", noHelpOutput)
}
@Test
fun `case sensitivity affects command matching`() {
val caseSensitiveParser = Parser("test", ParserConfig(caseSensitive = true))
val caseInsensitiveParser = Parser("test", ParserConfig(caseSensitive = false))
val testCmd = TestUtils.TestSubcommand("Test") // capital T
caseSensitiveParser.subcommands(testCmd)
caseInsensitiveParser.subcommands(testCmd)
// Case sensitive should not match "test" to "Test"
val sensitiveOutput = TestUtils.captureOutput {
caseSensitiveParser.parse(arrayOf("test", "--required", "value", "input.txt"))
}
assertTrue(sensitiveOutput.contains("Unknown command"))
assertFalse(testCmd.executed)
// Case insensitive should match
testCmd.executed = false // reset
caseInsensitiveParser.parse(arrayOf("test", "--required", "value", "input.txt"))
assertTrue(testCmd.executed)
}
}

View File

@@ -0,0 +1,175 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertThrows
import org.kargs.ArgumentParseException
import org.kargs.Parser
import org.kargs.ParserConfig
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
class ParserTest {
private lateinit var parser: Parser
private lateinit var testCmd: TestUtils.TestSubcommand
@BeforeEach
fun setup() {
parser = Parser("testapp")
testCmd = TestUtils.TestSubcommand()
parser.subcommands(testCmd)
}
@Test
fun `parse basic command with required option`() {
parser.parse(arrayOf("test", "--required", "value", "input.txt"))
assertTrue(testCmd.executed)
assertEquals("value", testCmd.requiredOpt)
assertEquals("input.txt", testCmd.inputArg)
}
@Test
fun `parse command with short options`() {
parser.parse(arrayOf("test", "-r", "required", "-s", "string", "-n", "42", "input.txt"))
assertTrue(testCmd.executed)
assertEquals("required", testCmd.requiredOpt)
assertEquals("string", testCmd.stringOpt)
assertEquals(42, testCmd.intOpt)
assertEquals("input.txt", testCmd.inputArg)
}
@Test
fun `parse command with flags`() {
parser.parse(arrayOf("test", "--required", "value", "--verbose", "--debug", "input.txt"))
assertTrue(testCmd.executed)
assertEquals(true, testCmd.verboseFlag)
assertEquals(true, testCmd.debugFlag)
assertEquals(false, testCmd.forceFlag) // not set, should be default
}
@Test
fun `parse command with combined short flags`() {
parser.parse(arrayOf("test", "-r", "value", "-vf", "input.txt"))
assertTrue(testCmd.executed)
assertEquals("value", testCmd.requiredOpt)
assertEquals(true, testCmd.verboseFlag)
assertEquals(true, testCmd.forceFlag)
}
@Test
fun `parse command with all argument types`() {
parser.parse(arrayOf(
"test",
"--required", "req",
"--string", "hello",
"--number", "123",
"--range", "5",
"--choice", "b",
"--double", "3.14",
"--verbose",
"input.txt",
"output.txt"
))
assertTrue(testCmd.executed)
assertEquals("req", testCmd.requiredOpt)
assertEquals("hello", testCmd.stringOpt)
assertEquals(123, testCmd.intOpt)
assertEquals(5, testCmd.rangeOpt)
assertEquals("b", testCmd.choiceOpt)
assertEquals(3.14, testCmd.doubleOpt)
assertEquals(true, testCmd.verboseFlag)
assertEquals("input.txt", testCmd.inputArg)
assertEquals("output.txt", testCmd.outputArg)
}
@Test
fun `throws exception when required option missing`() {
assertThrows<ArgumentParseException> {
parser.parse(arrayOf("test", "input.txt"))
}
assertFalse(testCmd.executed)
}
@Test
fun `throws exception on invalid integer`() {
assertThrows<ArgumentParseException> {
parser.parse(arrayOf("test", "--required", "value", "--number", "not-a-number", "input.txt"))
}
}
@Test
fun `throws exception on out of range value`() {
assertThrows<ArgumentParseException> {
parser.parse(arrayOf("test", "--required", "value", "--range", "15", "input.txt"))
}
}
@Test
fun `throws exception on invalid choice`() {
assertThrows<ArgumentParseException> {
parser.parse(arrayOf("test", "--required", "value", "--choice", "invalid", "input.txt"))
}
}
@Test
fun `handles unknown command`() {
val output = TestUtils.captureOutput {
parser.parse(arrayOf("unknown", "arg"))
}
assertTrue(output.contains("Unknown command: unknown"))
}
@Test
fun `shows global help on empty args`() {
val output = TestUtils.captureOutput {
parser.parse(arrayOf())
}
assertTrue(output.contains("Usage: testapp <command>"))
assertTrue(output.contains("test"))
}
@Test
fun `shows global help on --help`() {
val output = TestUtils.captureOutput {
parser.parse(arrayOf("--help"))
}
assertTrue(output.contains("Usage: testapp <command>"))
}
@Test
fun `handles command aliases`() {
val aliasCmd = TestUtils.TestSubcommand("build", "Build command", listOf("b", "compile"))
parser.subcommands(aliasCmd)
// Test main name
parser.parse(arrayOf("build", "--required", "value", "input.txt"))
assertTrue(aliasCmd.executed)
// Reset and test alias
aliasCmd.executed = false
parser.parse(arrayOf("b", "--required", "value", "input.txt"))
assertTrue(aliasCmd.executed)
}
@Test
fun `handles missing option value`() {
assertThrows<ArgumentParseException> {
parser.parse(arrayOf("test", "--required"))
}
}
@Test
fun `handles missing short option value`() {
assertThrows<ArgumentParseException> {
parser.parse(arrayOf("test", "-r"))
}
}
}

View File

@@ -0,0 +1,159 @@
package org.kargs.tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.kargs.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import kotlin.test.assertNull
class SubcommandTest {
@Test
fun `subcommand creation with valid parameters`() {
val cmd = TestUtils.TestSubcommand("build", "Build the project", listOf("b", "compile"))
assertEquals("build", cmd.name)
assertEquals("Build the project", cmd.description)
assertEquals(listOf("b", "compile"), cmd.aliases)
}
@Test
fun `subcommand throws on blank name`() {
assertThrows<IllegalArgumentException> {
TestUtils.TestSubcommand("", "Description")
}
assertThrows<IllegalArgumentException> {
TestUtils.TestSubcommand(" ", "Description") // whitespace only
}
}
@Test
fun `subcommand registers properties correctly`() {
val cmd = TestUtils.TestSubcommand()
// Properties should be registered automatically via delegates
assertTrue(cmd.options.isNotEmpty())
assertTrue(cmd.flags.isNotEmpty())
assertTrue(cmd.arguments.isNotEmpty())
// Check specific properties are registered
assertTrue(cmd.options.any { it.longName == "required" })
assertTrue(cmd.flags.any { it.longName == "verbose" })
assertTrue(cmd.arguments.any { it.name == "input" })
}
@Test
fun `subcommand validation works`() {
val cmd = TestUtils.TestSubcommand()
// Should have validation errors for required fields
val errors = cmd.validate()
assertTrue(errors.isNotEmpty())
assertTrue(errors.any { it.contains("required") })
// Set required values through parsing (the proper way)
val requiredOption = cmd.options.find { it.longName == "required" }
val inputArgument = cmd.arguments.find { it.name == "input" }
requiredOption?.parseValue("value")
inputArgument?.parseValue("input.txt")
// Should now be valid
val errorsAfter = cmd.validate()
assertTrue(errorsAfter.isEmpty())
}
@Test
fun `subcommand help generation`() {
val cmd = TestUtils.TestSubcommand("test", "Test command for validation")
val helpOutput = TestUtils.captureOutput {
cmd.printHelp()
}
assertTrue(helpOutput.contains("Usage: test [options]"))
assertTrue(helpOutput.contains("Test command for validation"))
assertTrue(helpOutput.contains("Options:"))
assertTrue(helpOutput.contains("--required"))
assertTrue(helpOutput.contains("Flags:"))
assertTrue(helpOutput.contains("--verbose"))
assertTrue(helpOutput.contains("Arguments:"))
assertTrue(helpOutput.contains("input"))
}
@Test
fun `subcommand property access works`() {
val cmd = TestUtils.TestSubcommand()
// Initially null/default values
assertNull(cmd.requiredOpt)
assertEquals(false, cmd.verboseFlag)
assertNull(cmd.inputArg)
// Set values through parsing (simulating real usage)
val requiredOption = cmd.options.find { it.longName == "required" }
val verboseFlag = cmd.flags.find { it.longName == "verbose" }
val inputArgument = cmd.arguments.find { it.name == "input" }
requiredOption?.parseValue("test")
verboseFlag?.setFlag()
inputArgument?.parseValue("file.txt")
// Values should be accessible
assertEquals("test", cmd.requiredOpt)
assertEquals(true, cmd.verboseFlag)
assertEquals("file.txt", cmd.inputArg)
}
@Test
fun `subcommand property types are correct`() {
val cmd = TestUtils.TestSubcommand()
// Check that we can find properties by their characteristics
val stringOption = cmd.options.find { it.longName == "string" }
val intOption = cmd.options.find { it.longName == "number" }
val rangeOption = cmd.options.find { it.longName == "range" }
val choiceOption = cmd.options.find { it.longName == "choice" }
assertTrue(stringOption != null)
assertTrue(intOption != null)
assertTrue(rangeOption != null)
assertTrue(choiceOption != null)
// Test parsing different types
stringOption?.parseValue("hello")
intOption?.parseValue("42")
rangeOption?.parseValue("5")
choiceOption?.parseValue("b")
assertEquals("hello", cmd.stringOpt)
assertEquals(42, cmd.intOpt)
assertEquals(5, cmd.rangeOpt)
assertEquals("b", cmd.choiceOpt)
}
@Test
fun `subcommand execution tracking works`() {
val cmd = TestUtils.TestSubcommand()
assertFalse(cmd.executed)
assertTrue(cmd.executionData.isEmpty())
// Set some values and execute
val requiredOption = cmd.options.find { it.longName == "required" }
val inputArgument = cmd.arguments.find { it.name == "input" }
requiredOption?.parseValue("test")
inputArgument?.parseValue("input.txt")
cmd.execute()
assertTrue(cmd.executed)
assertTrue(cmd.executionData.isNotEmpty())
assertEquals("test", cmd.executionData["requiredOpt"])
assertEquals("input.txt", cmd.executionData["inputArg"])
}
}

View File

@@ -0,0 +1,73 @@
package org.kargs.tests
import org.kargs.*
import java.io.ByteArrayOutputStream
import java.io.PrintStream
/**
* Utility functions for testing
*/
object TestUtils {
/**
* Capture console output during execution
*/
fun captureOutput(block: () -> Unit): String {
val originalOut = System.out
val outputStream = ByteArrayOutputStream()
System.setOut(PrintStream(outputStream))
try {
block()
return outputStream.toString().trim()
} finally {
System.setOut(originalOut)
}
}
/**
* Create a test subcommand for testing purposes
*/
class TestSubcommand(
name: String = "test",
description: String = "Test command",
aliases: List<String> = emptyList()
) : Subcommand(name, description, aliases) {
var executed = false
var executionData: Map<String, Any?> = emptyMap()
// Test properties
val stringOpt by Option(ArgType.String, "string", "s", "String option")
val requiredOpt by Option(ArgType.String, "required", "r", "Required option", required = true)
val intOpt by Option(ArgType.Int, "number", "n", "Integer option")
val rangeOpt by Option(ArgType.intRange(1, 10), "range", description = "Range option")
val choiceOpt by Option(ArgType.choice("a", "b", "c"), "choice", "c", "Choice option")
val doubleOpt by Option(ArgType.Double, "double", "d", "Double option")
val verboseFlag by Flag("verbose", "v", "Verbose flag")
val debugFlag by Flag("debug", description = "Debug flag")
val forceFlag by Flag("force", "f", "Force flag", defaultValue = false)
val inputArg by Argument(ArgType.String, "input", "Input argument")
val outputArg by Argument(ArgType.String, "output", "Output argument", required = false)
override fun execute() {
executed = true
executionData = mapOf(
"stringOpt" to stringOpt,
"requiredOpt" to requiredOpt,
"intOpt" to intOpt,
"rangeOpt" to rangeOpt,
"choiceOpt" to choiceOpt,
"doubleOpt" to doubleOpt,
"verboseFlag" to verboseFlag,
"debugFlag" to debugFlag,
"forceFlag" to forceFlag,
"inputArg" to inputArg,
"outputArg" to outputArg
)
}
}
}