/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.
package std.unittest
import std.sync.AtomicOptionReference
import std.process.Process
import std.unicode.*
import std.unittest.common.*
import std.random.Random
import std.unittest.prop_test.*
import std.unittest.common.KeyTags
func fillAbsentValues(config: Configuration) {
config.setIfAbsent(KeyGenerationSteps.generationSteps, 200)
config.setIfAbsent(KeyReductionSteps.reductionSteps, 200)
config.setIfAbsent(KeyWarmup.warmup, Duration.second)
config.setIfAbsent(KeyBatchSize.batchSize, 1..MAX_BATCH_SIZE)
config.setIfAbsent(KeyMinDuration.minDuration, Duration.second * 5)
config.setIfAbsent(KeyMinBatches.minBatches, 10)
config.setIfAbsent(KeyExplicitGC.explicitGC, ExplicitGcType.Auto)
config.setIfAbsent(KeyMeasurementInfo.measurementInfo, TimeNow().info)
}
func kebabCaseToCamelCase(str: String) {
let sb: StringBuilder = StringBuilder()
let split = str.lazySplit("-", removeEmpty: true)
match (split.next()) {
case None => return ""
case Some(fragment) => sb.append(fragment)
}
for (fragment in split) {
sb.append(fragment.toAsciiTitle())
}
return sb.toString()
}
func camelCaseToKebabCase(str: String): String {
let sb = StringBuilder()
for (c in str.runes()) {
if (c.isUpperCase()) {
sb.append('-')
sb.append(c.toLowerCase())
} else {
sb.append(c)
}
}
return sb.toString()
}
func setValueWithGuessedType(configuration: Configuration, key: String, value: String) {
try {
let jsv = JsonValue.fromStr(value)
match (jsv.kind()) {
case JsBool => configuration.setByName(key, jsv.asBool().getValue())
case JsInt => configuration.guessInteger(key, jsv.asInt().getValue())
case JsFloat => configuration.guessFloat(key, jsv.asFloat().getValue())
case JsString => configuration.setByName(key, jsv.asString().getValue())
// else just use String
case _ => configuration.setByName(key, value)
}
} catch (ex: JsonException) {
// ignore the error, just use String
configuration.setByName(key, value)
}
}
func buildDefaultConfiguration(): Configuration {
let programArgs = Process.current.arguments
let result = Configuration()
for (arg in programArgs) {
if (arg.startsWith("--")) {
let kv = arg[2..].split("=", 2, removeEmpty: true)
if (kv.size < 1 || kv.size > 2) {
continue
}
let k = kebabCaseToCamelCase(kv[0])
let v = if (kv.size == 2) { kv[1] } else { "true" }
if (k == "jsonConfiguration") {
setValuesFromJsonConfiguration(result, v)
} else {
setValueWithGuessedType(result, k, v)
}
}
}
fillTimeoutKey(result)
fillAbsentValues(result)
return result
}
var defaultConfigurationCached: AtomicOptionReference<Configuration> = AtomicOptionReference<Configuration>()
/**
* Create default configuration by reading command-line arguments.
* Any command-line argument given to the test will be turned into a field in configuration according to the rules:
* - kebab-case arguments such as --random-seed are turned into camelCase parameters: randomSeed
* - arguments with no values are turned into bool values: --no-color becomes noColor of type Bool and value true
* - the rest are converted according to the following rules, in this order:
* - `true` and `false` values are Bool: --feel-good=true becomes field feelGood of type Bool and value true
* - integer number literals are Int64: --random-seed=3 becomes field randomSeed of type Int64 and value 3
* - float number literals are Float64: --value-pi=3.14 becomes field valuePi of type Float64 and value 3.14
* - any other values are considered Strings
*/
public func defaultConfiguration(): Configuration {
// the idea behind this function is the following: we don't want a lock, because
// it's too heavy, but we also want to avoid running buildDefaultConfiguration()
// too many times (several is fine), so we implement double checking using CAS on AtomicOptionReference
match (defaultConfigurationCached.load()) {
// it is already set, return it
case Some(c) => return c
// it is not set, try setting it
case None =>
unittestOptionsRegistryClosed = true
let newValue = buildDefaultConfiguration()
// we have a race here, someone could have already assigned the value, so we need to do CAS
if (defaultConfigurationCached.compareAndSwap(None, newValue)) {
// CAS was successful, we can just return new value
return newValue
} else {
// CAS is not successful, someone has assigned the value before us, try again
// this can only recurse one time, so should be fine
return defaultConfiguration()
}
}
}
// this func mutate configuration
func setValuesFromJsonConfiguration(configuration: Configuration, rawJson: String) {
let jsonObject = JsonValue.fromStr(rawJson).asObject()
for ((key, jsonValue) in jsonObject.getFields()) {
let camelKey = kebabCaseToCamelCase(key)
match (jsonValue) {
case bool: JsonBool => configuration.setIfAbsentByName(camelKey, bool.getValue())
case int: JsonInt => configuration.setIfAbsentByName(camelKey, int.getValue())
case float: JsonFloat => configuration.setIfAbsentByName(camelKey, float.getValue())
case string: JsonString => configuration.setIfAbsentByName(camelKey, string.getValue())
case _ => throw UnittestCliOptionsFormatException("Unparsable configuration value ${jsonValue}")
}
}
}
private enum Guesser {
| Success
| Fail(Exception)
static func tryDefault(cb: () -> Unit): Guesser {
try {
cb()
return Success
} catch (e: Exception) {
return Fail(e)
}
}
func orTry(cb: () -> Unit): Guesser {
match (this) {
case Success => Success
case Fail(_) =>
try {
cb()
return Success
} catch (_: Exception) {
return this // keep the first exception
}
}
}
func orFail(): Unit {
match (this) {
case Success => ()
case Fail(exception) => throw exception
}
}
}
extend Configuration {
func setIfAbsent<T>(key: KeyFor<T>, value: T): Unit {
match (get<T>(key)) {
case Some(_) => ()
case None => set(key, value)
}
}
func setIfAbsentByName<T>(name: String, value: T): Unit {
match (getByName<T>(name)) {
case Some(_) => ()
case None => setByName(name, value)
}
}
func guessInteger(name: String, value: Int64): Unit {
Guesser.tryDefault { setByName(name, value) }
.orTry { setByName(name, Int32(value)) }
.orTry { setByName(name, Int16(value)) }
.orTry { setByName(name, Int8(value)) }
.orTry { setByName(name, UInt64(value)) }
.orTry { setByName(name, UInt32(value)) }
.orTry { setByName(name, UInt16(value)) }
.orTry { setByName(name, UInt8(value)) }
.orFail()
}
func guessFloat(name: String, value: Float64): Unit {
Guesser.tryDefault { setByName(name, value) }
.orTry { setByName(name, Float32(value)) }
.orTry { setByName(name, Float16(value)) }
.orFail()
}
}
func typeValidator<T>(name: String): (Any) -> OptionValidity {
{ x: Any =>
match (x) {
case t: T => return OptionValidity.ValidOption(ConfigurationKey.create<T>(name))
case _ => return OptionValidity.UnknownOptionType
}
}
}
func registerUnittestOption<T>(
key: KeyFor<T>,
description!: ?String = None,
typeDescription!: ?(String, String) = None
): Unit {
registerOptionValidator(key.name, typeValidator<T>(key.name))
if (let Some(typeDescription) <- typeDescription) {
setOrUpdateOptionInfo(key.name, description, typeDescription[0], typeDescription[1])
}
}
@When[os != "iOS"]
func registerParallelOption() {
registerUnittestOption<Bool>(KeyParallel.parallel, description: "Specify parallelism of tests execution",
typeDescription: ("Bool", "Run tests in parallel"))
registerUnittestOption<Int64>(KeyParallel.parallel,
typeDescription: ("Int64", "Run tests in parallel on %n processes"))
registerUnittestOption<String>(KeyParallel.parallel,
typeDescription: ("String", "Run tests in parallel on %n * actual number of cores, e.g. '--parallel=0.5nCores'"))
}
@When[os == "iOS"]
func registerParallelOption() {
}
@When[os != "iOS"]
func registerTimeoutEachOption() {
registerUnittestOption<String>(KeyTimeoutEach.timeoutEach,
typeDescription: ("String",
"Execution time limit for each test case ('millis' | 's' | 'm' | 'h'), e.g. '10s', '9millis'"))
}
@When[os == "iOS"]
func registerTimeoutEachOption() {
}
@When[os != "iOS"]
func registerCaptureOutputOption() {
registerUnittestOption<Bool>(KeyCaptureOutput.captureOutput,
typeDescription: ("Bool", "Disable immediate printing of test output, useful with --parallel"))
}
@When[os == "iOS"]
func registerCaptureOutputOption() {
}
@When[os != "iOS"]
func registerDeathAwareOption() {
registerUnittestOption<Bool>(KeyDeathAware.deathAware,
typeDescription: ("Bool", "Handle abnormal tests exits, e.g. `Process.current.exit(42)`"))
}
@When[os == "iOS"]
func registerDeathAwareOption() {
}
let _ = { =>
registerUnittestOption<Bool>(KeyHelp.help)
registerUnittestOption<Bool>(KeyNoColor.noColor, typeDescription: ("Bool", "Print the output colorless"))
registerUnittestOption<Int64>(KeyRandomSeed.randomSeed,
typeDescription: ("Int64", "Seed for deterministic test reproduction"))
registerUnittestOption<RandomSource>(KeyRandom.random)
registerParallelOption()
registerUnittestOption<Int64>(KeyInternalWorkerId.internalWorkerId)
registerUnittestOption<Int64>(KeyInternalNWorkers.internalNWorkers)
registerUnittestOption<Int64>(KeyInternalWorkerSkipNTests.internalWorkerSkipNTests)
registerUnittestOption<UInt16>(KeyInternalMainProcessPort.internalMainProcessPort)
registerUnittestOption<String>(KeyInternalTestrunnerInputPath.internalTestrunnerInputPath)
registerUnittestOption<Duration>(KeyTimeout.timeout)
registerTimeoutEachOption()
registerUnittestOption<(TestCaseInfo) -> Unit>(KeyTimeoutHandler.timeoutHandler)
registerUnittestOption<Array<String>>(KeyTags.tags)
registerUnittestOption<String>(KeyFilter.filter,
typeDescription: ("String", "Running a filtered by name subset of tests, e.g. '--filter=*.*Test,*.*case*'"))
registerUnittestOption<String>(KeyIncludeTags.includeTags,
typeDescription: ("String", "Include only tests with tags e.g. '--include-tags=Unittest+Smoke'"))
registerUnittestOption<String>(KeyExcludeTags.excludeTags,
typeDescription: ("String", "Exclude tests with tags e.g. '--exclude-tags=Performance,Network'"))
registerCaptureOutputOption()
registerUnittestOption<Bool>(KeyNoCaptureOutput.noCaptureOutput,
typeDescription: ("Bool", "Immediately print test output"))
registerUnittestOption<Bool>(KeyShowAllOutput.showAllOutput,
typeDescription: ("Bool", "Print output even for passed tests"))
registerUnittestOption<Bool>(KeyVerbose.verbose, typeDescription: ("Bool", "Print tests execution status"))
registerUnittestOption<Bool>(KeySkip.skip)
registerUnittestOption<Bool>(KeyDryRun.dryRun, typeDescription: ("Bool", "Print tests without execution"))
registerUnittestOption<Bool>(KeyShowTags.showTags, typeDescription: ("Bool", "Print tags in text report"))
registerUnittestOption<Bool>(KeyFromTopLevel.fromTopLevel)
registerUnittestOption<Int64>(KeyGenerationSteps.generationSteps)
registerUnittestOption<Int64>(KeyReductionSteps.reductionSteps)
registerUnittestOption<Bool>(KeyCoverageGuided.coverageGuided)
registerUnittestOption<Int64>(KeyCoverageGuidedInitialSeeds.coverageGuidedInitialSeeds)
registerUnittestOption<Int64>(KeyCoverageGuidedMaxCandidates.coverageGuidedMaxCandidates)
registerUnittestOption<Int64>(KeyCoverageGuidedBaselineScore.coverageGuidedBaselineScore)
registerUnittestOption<Int64>(KeyCoverageGuidedNewCoverageScore.coverageGuidedNewCoverageScore)
registerUnittestOption<Int64>(KeyCoverageGuidedNewCoverageBonus.coverageGuidedNewCoverageBonus)
registerDeathAwareOption()
registerUnittestOption<String>(KeyReportPath.reportPath,
typeDescription: ("String", "Path where to create report file"))
registerUnittestOption<String>(KeyReportFormat.reportFormat,
typeDescription: ("String", "Report format ('xml' for tests), ('csv', 'csw-raw', 'html' for benchmarks)"))
registerUnittestOption<Bool>(KeyBench.bench,
typeDescription: ("Bool", "Run only benchmarks (tests marked with @Bench)"))
registerUnittestOption<String>(KeyBaselinePath.baselinePath,
typeDescription: ("String", "Path of previous benchmark report to which to compare"))
registerUnittestOption<String>(KeyBaseline.baseline)
registerUnittestOption<Int64>(KeyBatchSize.batchSize)
registerUnittestOption<Range<Int64>>(KeyBatchSize.batchSize)
registerUnittestOption<Int64>(KeyWarmup.warmup)
registerUnittestOption<Duration>(KeyWarmup.warmup)
registerUnittestOption<Duration>(KeyMinDuration.minDuration)
registerUnittestOption<ExplicitGcType>(KeyExplicitGC.explicitGC)
registerUnittestOption<Int64>(KeyMinBatches.minBatches)
registerUnittestOption<Measurement>(KeyMeasurement.measurement)
registerUnittestOption<MeasurementInfo>(KeyMeasurementInfo.measurementInfo)
// Dynamic progress
registerUnittestOption<Bool>(KeyNoProgress.noProgress, description: "Disable progress report",
typeDescription: ("Bool", "Disable progress report"))
registerUnittestOption<Bool>(KeyProgressBrief.progressBrief,
description: "Display brief progress report (single-line) instead of detailed one. Has lower precedence than `--no-progress`",
typeDescription: ("Bool", "Do display single-line progress report"))
registerUnittestOption<Int64>(KeyProgressEntriesLimit.progressEntriesLimit,
typeDescription: ("Int64", "Limit value. Value 0 means no limit. Default: 0"),
description: "Limit the entries amount displayed in the progress report. Has lower precedence than `--progress-brief`")
registerUnittestOption<Bool>(KeyOptimizeMocksForBench.optimizeMocksForBench,
typeDescription: ("Bool", "Optimize mocks when using benchmarks"))
}()