isol.2024.9.1.source-code.Submission.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jenisol Show documentation
Show all versions of jenisol Show documentation
Solution-driven autograding for CS 124.
The newest version!
@file:Suppress("InvalidPackageDeclaration")
package edu.illinois.cs.cs125.jenisol.core
import com.rits.cloning.Cloner
import edu.illinois.cs.cs125.jenisol.core.generators.Complexity
import edu.illinois.cs.cs125.jenisol.core.generators.Generators
import edu.illinois.cs.cs125.jenisol.core.generators.ObjectGenerator
import edu.illinois.cs.cs125.jenisol.core.generators.ReceiverGenerator
import edu.illinois.cs.cs125.jenisol.core.generators.TypeGeneratorGenerator
import edu.illinois.cs.cs125.jenisol.core.generators.Value
import edu.illinois.cs.cs125.jenisol.core.generators.getArrayDimension
import java.lang.RuntimeException
import java.lang.reflect.Constructor
import java.lang.reflect.Executable
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Type
import java.util.TreeMap
import kotlin.random.Random
import kotlin.reflect.full.memberFunctions
@Suppress("LargeClass")
class Submission(val solution: Solution, val submission: Class<*>) {
private val isKotlin = submission.getAnnotation(Metadata::class.java) != null
@Suppress("LongMethod", "CyclomaticComplexMethod", "ThrowsCount")
private fun checkClassDesign(solution: Class<*>, submission: Class<*>, innerClass: Boolean = false) {
if (!solution.visibilityMatches(submission)) {
throw SubmissionDesignClassError(
submission,
"is not ${
solution.getVisibilityModifier() ?: if (isKotlin) {
"internal"
} else {
"package private"
}
}",
innerClass,
)
}
if (!isKotlin && (solution.isFinal() != submission.isFinal())) {
throw SubmissionDesignClassError(
submission,
if (solution.isFinal()) {
"is not marked as final but should be"
} else {
"is marked as final but should not be"
},
innerClass,
)
}
if (solution.isAbstract() != submission.isAbstract()) {
throw SubmissionDesignClassError(
submission,
if (solution.isAbstract()) {
"is not marked as abstract but should be"
} else {
"is marked as abstract but should not be"
},
innerClass,
)
}
if (solution.isStatic() != submission.isStatic()) {
throw SubmissionDesignClassError(
submission,
if (solution.isStatic()) {
"is not marked as static but should be"
} else {
"is marked as static but should not be"
},
innerClass,
)
}
if (solution.superclass != null && solution.superclass != submission.superclass) {
throw SubmissionDesignClassError(
submission,
"does not extend ${solution.superclass.name}",
innerClass,
)
}
val solutionInterfaces = solution.interfaces.toSet()
val submissionInterfaces = submission.interfaces.toSet()
val missingInterfaces = solutionInterfaces.minus(submissionInterfaces)
if (missingInterfaces.isNotEmpty()) {
throw SubmissionDesignClassError(
submission,
"does not implement ${missingInterfaces.joinToString(separator = ", ") { it.name }}",
innerClass,
)
}
val extraInterfaces = submissionInterfaces.minus(solutionInterfaces)
if (extraInterfaces.isNotEmpty()) {
throw SubmissionDesignClassError(
submission,
"implements extra interfaces ${extraInterfaces.joinToString(separator = ", ") { it.name }}",
innerClass,
)
}
solution.typeParameters.forEachIndexed { i, type ->
@Suppress("TooGenericExceptionCaught", "SwallowedException")
try {
if (!submission.typeParameters[i].bounds.contentEquals(type.bounds)) {
throw SubmissionTypeParameterError(submission, innerClass)
}
} catch (e: Exception) {
throw SubmissionTypeParameterError(submission, innerClass)
}
}
if (submission.typeParameters.size > solution.typeParameters.size) {
throw SubmissionTypeParameterError(submission, innerClass)
}
}
init {
checkClassDesign(solution.solution, submission)
solution.solution.declaredClasses.filter { klass -> !klass.isPrivate() }.forEach { solutionInnerClass ->
val submissionInnerClass =
submission.declaredClasses.find { klass -> klass.simpleName == solutionInnerClass.simpleName }
if (submissionInnerClass == null) {
throw SubmissionDesignMissingInnerClassError(submission, solutionInnerClass)
}
checkClassDesign(solutionInnerClass, submissionInnerClass)
}
submission.declaredClasses.filter { klass -> !klass.isPrivate() }.forEach { submissionInnerClass ->
val solutionInnerClass = solution.solution.declaredClasses
.find { klass -> klass.simpleName == submissionInnerClass.simpleName }
if (solutionInnerClass == null) {
if (!(submission.isKotlin() && submissionInnerClass.kotlin.isCompanion)) {
throw SubmissionDesignExtraInnerClassError(submission, submissionInnerClass)
}
}
}
}
init {
solution.bothExecutables.forEach {
if (!it.parameterTypes[0].isAssignableFrom(submission)) {
throw SubmissionDesignInheritanceError(
submission,
it.parameterTypes[0],
)
}
}
}
private val submissionFields =
solution.allFields.filter { it.name != "${"$"}assertionsDisabled" }.map { solutionField ->
submission.findField(solutionField) ?: throw SubmissionDesignMissingFieldError(
submission,
solutionField,
)
}.toSet()
private val hasInnerClasses = submission.declaredClasses.any { klass ->
!(submission.isKotlin() && klass.kotlin.isCompanion)
}
val submissionExecutables = solution.allExecutables
.filter {
!isKotlin || (!solution.skipReceiver || it !in solution.receiverGenerators)
}.associate { solutionExecutable ->
when (solutionExecutable) {
is Constructor<*> -> submission.findConstructor(solutionExecutable, solution.solution)
is Method -> submission.findMethod(solutionExecutable, solution.solution)
else -> error("Encountered unexpected executable type: $solutionExecutable")
}?.let { executable ->
executable.isAccessible = true
solutionExecutable to executable
} ?: run {
@Suppress("ComplexCondition")
if (isKotlin &&
solutionExecutable is Method &&
(solutionExecutable.looksLikeGetter() || solutionExecutable.looksLikeSetter())
) {
val field = solutionExecutable.getterOrSetterToPropertyName()
if (solutionExecutable.looksLikeGetter()) {
throw SubmissionDesignKotlinNotAccessibleError(submission, field)
} else {
throw SubmissionDesignKotlinNotModifiableError(submission, field)
}
} else {
throw SubmissionDesignMissingMethodError(submission, solutionExecutable, hasInnerClasses)
}
}
}.toMutableMap().also {
if (solution.initializer != null) {
it[solution.initializer] = solution.initializer
}
}.toMap().also { executableMap ->
val kotlinReflectionSupported = isKotlin &&
try {
submission.kotlin.memberFunctions
true
} catch (e: UnsupportedOperationException) {
false
}
if (kotlinReflectionSupported) {
@Suppress("MagicNumber")
executableMap.keys
.filterIsInstance()
.filter { it.looksLikeGetter() || it.looksLikeSetter() }
.firstOrNull { method ->
submission.kotlin.memberFunctions.map { it.name }.contains(method.name)
}
?.also { method ->
val field = method.getterOrSetterToPropertyName()
if (!method.hasKotlinMirrorOK()) {
throw KotlinBadSetterOrGetter(field, method.name)
}
}
}
}
init {
if (submission != solution.solution) {
(submission.declaredMethods.toSet() + submission.declaredConstructors.toSet()).filter {
!it.isPrivate() && !it.isSynthetic && !(it is Method && it.isBridge)
}.forEach { executable ->
if (executable !in submissionExecutables.values) {
if (isKotlin) {
// HACK to work around Kotlin's lack of setter-only syntax
if (executable is Method && executable.looksLikeGetter()) {
val setterName = executable.name.replace("get", "set")
if (submissionExecutables.values.map { it.name }.contains(setterName)) {
return@forEach
}
}
if (solution.skipReceiver && executable is Constructor<*>) {
return@forEach
}
if (executable.isKotlinCompanionAccessor()) {
return@forEach
}
if (executable is Constructor<*> &&
executable.parameterTypes.lastOrNull()?.name ==
"kotlin.jvm.internal.DefaultConstructorMarker"
) {
return@forEach
}
@Suppress("EmptyCatchBlock")
try {
if (submission.kotlin.isData && executable.isDataClassGenerated()) {
return@forEach
}
} catch (_: UnsupportedOperationException) {
}
if (executable.name == "compareTo") {
return@forEach
}
}
@Suppress("ComplexCondition", "MagicNumber")
if (isKotlin &&
executable is Method &&
(executable.looksLikeGetter() || executable.looksLikeSetter())
) {
if (executable.looksLikeSetter()) {
throw SubmissionDesignKotlinIsModifiableError(
submission,
executable.getterOrSetterToPropertyName(),
)
} else {
throw SubmissionDesignKotlinIsAccessibleError(
submission,
executable.getterOrSetterToPropertyName(),
)
}
}
throw SubmissionDesignExtraMethodError(
submission,
executable,
)
}
}
submission.declaredFields.toSet().filter {
it.name != "${"$"}assertionsDisabled" &&
!(isKotlin && it.name == "Companion")
}.forEach {
if (!it.isPrivate() && it !in submissionFields) {
throw SubmissionDesignExtraFieldError(submission, it)
}
if (it.isStatic()) {
if (!solution.skipReceiver) {
throw SubmissionStaticFieldError(submission, it)
} else if (!it.isPrivate()) {
throw SubmissionStaticPublicFieldError(submission, it)
}
}
}
}
}
private val comparators = Comparators(
mutableMapOf(solution.solution to solution.receiverCompare, submission to solution.receiverCompare),
)
fun compare(solution: Any?, submission: Any?, solutionClass: Class<*>? = null, submissionClass: Class<*>? = null) =
when (solution) {
null -> submission == null
else -> solution.deepEquals(submission, comparators, solutionClass, submissionClass)
}
fun verify(executable: Executable, result: TestResult<*, *>) {
solution.verifiers[executable]?.also { customVerifier ->
@Suppress("TooGenericExceptionCaught")
try {
unwrap { customVerifier.invoke(null, result) }
} catch (@Suppress("DEPRECATION") e: ThreadDeath) {
throw e
} catch (e: Throwable) {
result.differs.add(TestResult.Differs.VERIFIER_THREW)
result.verifierThrew = e
}
} ?: run {
defaultVerify(result)
}
}
@Suppress("ComplexMethod", "LongMethod")
private fun defaultVerify(result: TestResult<*, *>) {
val solution = result.solution
val submission = result.submission
val strictOutput = result.solutionExecutable.annotations.find { it is Configure }?.let {
(it as Configure).strictOutput
} ?: false
if (!compare(solution.threw, submission.threw, result.solutionClass, result.submissionClass)) {
result.differs.add(TestResult.Differs.THREW)
}
if ((strictOutput || solution.stdout.isNotBlank()) && solution.stdout != submission.stdout) {
result.differs.add(TestResult.Differs.STDOUT)
if (solution.stdout == submission.stdout + "\n") {
result.message = if (result.submissionIsKotlin) {
"Output is missing a newline, maybe use println instead of print?"
} else {
"Output is missing a newline, maybe use System.out.println instead of System.out.print?"
}
}
if (solution.stdout + "\n" == submission.stdout) {
result.message = if (result.submissionIsKotlin) {
"Output has an extra newline, maybe use print instead of println?"
} else {
"Output has an extra newline, maybe use System.out.print instead of System.out.println?"
}
}
}
if ((strictOutput || solution.stderr.isNotBlank()) && solution.stderr != submission.stderr) {
result.differs.add(TestResult.Differs.STDERR)
if (solution.stderr == submission.stderr + "\n") {
result.message =
"Error output is missing a newline, maybe use System.err.println instead of System.err.print?"
}
if (solution.stderr + "\n" == submission.stderr) {
result.message =
"Error output has an extra newline, maybe use System.err.print instead of System.err.println?"
}
}
@Suppress("ComplexCondition")
if ((strictOutput || solution.stdout.isNotBlank() || solution.stderr.isNotBlank()) &&
solution.interleavedOutput != submission.interleavedOutput
) {
result.differs.add(TestResult.Differs.INTERLEAVED_OUTPUT)
}
if (result.existingReceiverMismatch) {
result.differs.add(TestResult.Differs.RETURN)
}
if (result.type == TestResult.Type.METHOD || result.type == TestResult.Type.STATIC_METHOD) {
val customCompare = if (solution.returned != null) {
this.solution.customCompares.entries.find { (type, _) ->
type.isAssignableFrom(solution.returned::class.java)
}
} else {
null
}?.value
if (customCompare != null && submission.returned != null) {
@Suppress("TooGenericExceptionCaught")
try {
customCompare.invoke(null, solution.returned, submission.returned)
} catch (e: Throwable) {
result.differs.add(TestResult.Differs.RETURN)
result.message = e.message
}
} else {
@Suppress("TooGenericExceptionCaught")
try {
compare(
solution.returned,
submission.returned,
result.solutionClass,
result.submissionClass,
).also {
if (!it) {
result.differs.add(TestResult.Differs.RETURN)
}
}
} catch (e: Throwable) {
result.differs.add(TestResult.Differs.RETURN)
result.message = e.message
}
}
}
if (result.type == TestResult.Type.FACTORY_METHOD &&
solution.returned != null &&
solution.returned::class.java.isArray
) {
@Suppress("ComplexCondition")
if (submission.returned == null ||
!submission.returned::class.java.isArray ||
solution.returned::class.java.getArrayDimension()
!= submission.returned::class.java.getArrayDimension() ||
(solution.returned as Array<*>).size != (submission.returned as Array<*>).size
) {
result.differs.add(TestResult.Differs.RETURN)
}
}
if (!compare(solution.parameters, submission.parameters, result.solutionClass, result.submissionClass)) {
result.differs.add(TestResult.Differs.PARAMETERS)
}
}
inner class ExecutablePicker(private val random: Random, private val methods: Set) {
private val counts: MutableMap = methods.filter {
it in solution.limits.keys
}.associateWith {
0
}.toMutableMap()
private val finished = mutableSetOf()
private lateinit var executableChooser: TreeMap
private var total: Double = 0.0
private fun setWeights() {
var setTotal = 0.0
val methodsLeft = methods - finished
executableChooser = TreeMap(
methodsLeft.associateWith { solution.defaultTestingWeight(it) }
.toSortedMap { e1, e2 ->
e1.fullName().compareTo(e2.fullName())
}
.map { (executable, weight) ->
setTotal += weight
setTotal to executable
}.toMap(),
)
total = setTotal
}
init {
setWeights()
}
private var previous: Executable? = null
fun next(): Executable {
check(more()) { "Ran out of methods to test due to @Limit annotations" }
var next = executableChooser.higherEntry(random.nextDouble() * total).value!!
if (next == previous && methods.size > 1) {
next = executableChooser.higherEntry(random.nextDouble() * total).value!!
}
previous = next
if (next in counts) {
counts[next] = counts[next]!! + 1
if (counts[next]!! == solution.limits[next]!!) {
finished += next
setWeights()
}
}
return next
}
fun more() = methods.size > finished.size
}
class RecordingRandom(seed: Long, private val follow: List? = null, private val record: Boolean = false) :
Random() {
private val random = Random(seed)
private val trace = mutableListOf()
var currentIndex = 0
var lastRandom = 0
@Suppress("ThrowingExceptionsWithoutMessageOrCause")
override fun nextBits(bitCount: Int): Int = random.nextBits(bitCount).also { newValue ->
if (record) {
trace += newValue
}
lastRandom = newValue
}.also { actualValue ->
if (follow != null) {
val expectedValue = follow.getOrNull(currentIndex)
if (expectedValue != actualValue) {
throw FollowTraceException(currentIndex, "expected $expectedValue, actual $actualValue")
}
}
currentIndex++
}
fun finish(): List = trace.toList()
}
fun findReceiver(runners: List, solutionReceiver: Any) = let {
check(solutionReceiver::class.java == solution.solution) {
"findReceiver should be passed an instance of the receiver class"
}
runners.find { it.receivers?.solution === solutionReceiver }
}
@Suppress("LongMethod", "ComplexMethod", "ReturnCount", "NestedBlockDepth", "ThrowsCount")
fun test(
passedSettings: Settings = Settings(),
captureOutputControlInput: CaptureOutputControlInput = ::defaultCaptureOutputControlInput,
followTrace: List? = null,
testingEventListener: TestingEventListener = {},
): TestResults {
if (solution.solution.isDesignOnly() || solution.solution.isAbstract()) {
throw DesignOnlyTestingError(solution.solution)
}
val settings = solution.setCounts(Settings.DEFAULTS merge passedSettings)
check(settings.runAll != null)
check(!(settings.runAll && settings.shrink!!)) {
"Running all tests combined with test shrinking produces inconsistent results"
}
val random = if (settings.seed == -1) {
RecordingRandom(Random.nextLong(), follow = followTrace, record = settings.recordTrace!!)
} else {
RecordingRandom(settings.seed.toLong(), follow = followTrace, record = settings.recordTrace!!)
}
val runners: MutableList = mutableListOf()
var stepCount = 0
val receiverGenerators = sequence {
while (true) {
yieldAll(solution.receiverGenerators.toList().shuffled(random))
}
}
val cloner = Cloner.shared()
val (receiverGenerator, generatorOverrides) = if (!solution.skipReceiver) {
val receiverGenerator = ReceiverGenerator(random, mutableListOf(), this@Submission)
val overrideMap = mutableMapOf(
(solution.solution as Type)
to ({ _: Random, _: Cloner -> receiverGenerator } as TypeGeneratorGenerator),
)
if (!solution.generatorFactory.typeGenerators.containsKey(Any::class.java)) {
overrideMap[(Any::class.java)] = { r: Random, c: Cloner ->
ObjectGenerator(
r,
c,
receiverGenerator,
)
}
}
Pair(receiverGenerator, overrideMap.toMap())
} else {
Pair>(null, mapOf())
}
val generators = solution.generatorFactory.get(random, cloner, generatorOverrides)
fun List.createdCount() =
count { it.created && (solution.skipReceiver || it.receivers?.solution != null) }
val neededReceivers = settings.receiverCount.coerceAtLeast(1)
var loopCount = 0
@Suppress("UNCHECKED_CAST", "LongParameterList", "MemberVisibilityCanBePrivate")
fun List.toResults(
completed: Boolean = false,
threw: Throwable? = null,
timeout: Boolean = false,
) = TestResults(
map { it.testResults as List> }.flatten().sortedBy { it.stepCount },
settings,
completed,
threw,
timeout,
runners.createdCount() >= neededReceivers,
count { !it.tested },
stepCount = stepCount,
skippedSteps = map { it.skippedTests }.flatten().sorted(),
loopCount = loopCount,
randomTrace = random.finish(),
)
@Suppress("TooGenericExceptionCaught")
try {
fun addRunner(generators: Generators, receivers: Value? = null) = TestRunner(
runners.size,
this@Submission,
generators,
receiverGenerators,
captureOutputControlInput,
ExecutablePicker(random, solution.methodsToTest),
settings,
runners,
receivers,
random,
testingEventListener,
).also { runner ->
if (receivers == null && !solution.skipReceiver) {
runner.next(stepCount++)
}
runners.add(runner)
}
var currentRunner: TestRunner? = null
if (solution.skipReceiver) {
addRunner(generators).also {
check(it.ready) { "Static method receivers should start ready" }
currentRunner = it
}
}
var totalCount = 0
var receiverStepCount = 0
var testStepCount = 0
var receiverIndex = 0
val transitionProbability = if (solution.fauxStatic || solution.skipReceiver) {
0.0
} else {
1.0 / (settings.methodCount.toDouble())
}
fun nextRunner(checkNull: Boolean = true) {
currentRunner = runners.filterIndexed { index, _ -> index > receiverIndex }.find { it.ready }
if (checkNull) {
check(currentRunner != null)
}
receiverIndex = runners.indexOf(currentRunner)
}
while (totalCount < settings.testCount) {
testingEventListener(StartLoop(loopCount++))
val finishedReceivers = runners.createdCount() >= neededReceivers
if (Thread.interrupted()) {
return runners.toResults(timeout = true)
}
val readyLeft =
runners.filterIndexed { index, runner -> index > receiverIndex && runner.ready }.size
val createReceiver = when {
currentRunner == null -> true
solution.receiverAsParameter && !finishedReceivers -> true
random.nextDouble() < transitionProbability -> true
else -> false
}
val switchReceivers = when {
createReceiver -> false
solution.skipReceiver -> false
readyLeft > 0 && random.nextDouble() < transitionProbability -> true
else -> false
}
if (createReceiver) {
check(!solution.skipReceiver) { "Static testing should never drop receivers" }
addRunner(generators).also { runner ->
@Suppress("UNCHECKED_CAST")
if (runner.ready) {
check(runner.receivers != null)
receiverGenerator?.receivers?.add(runner.receivers as Value)
}
}.also {
if (it.failed) {
if ((!settings.shrink!! || it.lastComplexity!!.level <= Complexity.MIN) &&
!settings.runAll
) {
return runners.toResults()
}
}
if (!solution.receiverAsParameter || currentRunner == null) {
currentRunner = it
receiverIndex = runners.indexOf(currentRunner)
}
if (it.ranLastTest || it.skippedLastTest) {
receiverStepCount++
totalCount++
}
}
} else {
if (switchReceivers) {
nextRunner()
}
currentRunner!!.next(stepCount++).also { runner ->
if (runner.ranLastTest || runner.skippedLastTest) {
testStepCount++
totalCount++
}
}
}
if (currentRunner!!.failed) {
if ((!settings.shrink!! || currentRunner!!.lastComplexity!!.level <= Complexity.MIN) &&
!settings.runAll
) {
return runners.toResults()
}
}
if (currentRunner!!.returnedReceivers != null) {
currentRunner!!.returnedReceivers!!.forEach { returnedReceiver ->
if (findReceiver(runners, returnedReceiver.solution!!) == null) {
addRunner(generators, returnedReceiver)
} else {
@Suppress("UNCHECKED_CAST")
receiverGenerator?.receivers?.add(returnedReceiver as Value)
}
}
currentRunner!!.returnedReceivers = null
}
if (currentRunner?.ready == false) {
nextRunner(false)
}
}
return runners.toResults(completed = true)
} catch (e: FollowTraceException) {
throw e
} catch (e: Throwable) {
if (settings.testing!!) {
throw e
}
return runners.toResults(threw = e)
}
}
}
sealed class SubmissionDesignError(
message: String,
@Suppress(
"unused",
) val hint: String = "",
) : RuntimeException(message)
class SubmissionDesignMissingMethodError(klass: Class<*>, executable: Executable, hasInnerClasses: Boolean) :
SubmissionDesignError(
"${klass.name} doesn't provide ${
if (executable is Method) {
"""${
if (executable.isStatic()) {
"class "
} else {
""
}
}method"""
} else {
"constructor"
}
}${executable.fullName(klass.isKotlin())}${
if (hasInnerClasses) {
" (submission defines inner classes)"
} else {
""
}
}.",
"Your submission is missing a method. Check your method signatures.",
)
class SubmissionDesignMissingInnerClassError(klass: Class<*>, innerClass: Class<*>) :
SubmissionDesignError(
"${klass.name} doesn't provide inner class ${innerClass.name}.",
"Your submission is missing an inner class.",
)
class SubmissionDesignKotlinNotAccessibleError(klass: Class<*>, field: String) :
SubmissionDesignError(
"Property $field on ${klass.name} is missing or not accessible.",
"Your submission has a missing property.",
)
class SubmissionDesignKotlinNotModifiableError(klass: Class<*>, field: String) :
SubmissionDesignError(
"Property $field on ${klass.name} is not modifiable.",
"Your submission has a misconfigured property.",
)
class SubmissionDesignKotlinIsAccessibleError(klass: Class<*>, field: String) :
SubmissionDesignError(
"Property $field on ${klass.name} is accessible but should not be.",
"Your submission has a extra unnecessary property.",
)
class SubmissionDesignKotlinIsModifiableError(klass: Class<*>, field: String) :
SubmissionDesignError(
"Property $field on ${klass.name} is modifiable but should not be.",
"Your submission has a misconfigured property.",
)
class SubmissionDesignExtraMethodError(klass: Class<*>, executable: Executable) :
SubmissionDesignError(
"${klass.name} provided extra ${
if (executable.isStatic() && !klass.isKotlin()) {
"static "
} else {
""
}
}${
if (executable is Method) {
"method"
} else {
"constructor"
}
} ${executable.fullName(klass.isKotlin())}.",
"Your submission provides an extra method.",
)
class SubmissionDesignExtraInnerClassError(klass: Class<*>, innerKlass: Class<*>) :
SubmissionDesignError(
"${klass.name} provided extra inner class ${innerKlass.name}.",
"Your submission provides an extra inner class.",
)
class SubmissionDesignInheritanceError(klass: Class<*>, parent: Class<*>) :
SubmissionDesignError(
"${klass.name} doesn't inherit from ${parent.name}.",
)
class SubmissionTypeParameterError(klass: Class<*>, innerClass: Boolean = false) :
SubmissionDesignError(
"${
if (innerClass) {
"Inner class "
} else {
""
}
}${klass.name} has missing, unnecessary, or incorrectly-bounded type parameters.",
)
class SubmissionDesignMissingFieldError(klass: Class<*>, field: Field) :
SubmissionDesignError(
"Field ${field.fullName()} is not accessible in ${klass.name} but should be.",
"Your submission has a missing field.",
)
class SubmissionDesignExtraFieldError(klass: Class<*>, field: Field) :
SubmissionDesignError(
"Field ${field.fullName()} is accessible in ${klass.name} but should not be.",
"Your submission has a misconfigured field",
)
class SubmissionStaticFieldError(klass: Class<*>, field: Field) :
SubmissionDesignError(
"Field ${field.fullName()} is static in ${klass.name}, " +
"but static fields are not permitted for this problem.",
"Your submission has an incorrect static field.",
)
class SubmissionStaticPublicFieldError(klass: Class<*>, field: Field) :
SubmissionDesignError(
"Static field ${field.fullName()} in ${klass.name} must be private.",
"Your submission has a misconfigured static field.",
)
class SubmissionDesignClassError(klass: Class<*>, message: String, innerClass: Boolean = false) :
SubmissionDesignError(
"${
if (innerClass) {
"Inner class "
} else {
""
}
}${klass.name} $message.",
"Your submission class is designed incorrectly.",
)
class DesignOnlyTestingError(klass: Class<*>) :
Exception(
"Solution class ${klass.name} is marked as design only.",
)
class KotlinBadSetterOrGetter(property: String, bad: String) :
SubmissionDesignError(
"Kotlin class should declare a property $property and not manually implement $bad",
"Your submission provides an unnecessary Java-style getter or setter.",
)
@Suppress("SwallowedException")
fun unwrap(run: () -> Any?): Any? = try {
run()
} catch (e: InvocationTargetException) {
throw e.cause ?: error("InvocationTargetException should have a cause")
}
fun Class<*>.isKotlin() = getAnnotation(Metadata::class.java) != null
class FollowTraceException(index: Int, message: String) :
RuntimeException(
"Random generator out of sync at index $index: $message",
)
@Suppress("MagicNumber")
fun Executable.looksLikeGetter() =
this is Method && name.length > 3 && name.startsWith("get") && parameters.isEmpty() && returnType.name != "void"
@Suppress("MagicNumber")
fun Executable.looksLikeSetter() =
this is Method && name.length > 3 && name.startsWith("set") && parameters.size == 1 && returnType.name == "void"
fun Method.getterOrSetterToPropertyName() = name
.removePrefix("set")
.removePrefix("get")
.let { it[0].lowercase() + it.substring(1) }