Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rubricClassName to grader info #118

Merged
merged 7 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class GradleSubmissionExporter @Inject constructor(
val filteredSubmissions = if (graderJar == null) {
submissions
} else {
submissions.filter { graderJar.rubricProviders.containsKey((it as JavaSubmission).submissionInfo.assignmentId) }
submissions.filter { graderJar.info.assignmentId == (it as JavaSubmission).submissionInfo.assignmentId }
}
for (submission in filteredSubmissions) {
writeSubmission(submission, graderJar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ import org.sourcegrade.jagr.core.compiler.java.plus

class FallbackRuntimeTester : RuntimeTester {
override fun createTestCycle(grader: GraderJarImpl, submission: Submission): TestCycle? {
val info = (submission as JavaSubmission).submissionInfo
val rubricProviders = grader.rubricProviders[info.assignmentId] ?: return null
submission as JavaSubmission
var resources = grader.container.runtimeResources
resources += submission.compileResult.runtimeResources + submission.libraries
val classLoader = RuntimeClassLoaderImpl(resources)
val notes = listOf(
"The grading process was forcibly terminated.",
"Please check if you have an infinite loop or infinite recursion.",
)
return FallbackTestCycle(rubricProviders, submission, classLoader, notes)
return FallbackTestCycle(grader.info.rubricProviderName, submission, classLoader, notes)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import org.sourcegrade.jagr.api.testing.TestCycle
import org.sourcegrade.jagr.core.compiler.java.RuntimeClassLoaderImpl

class FallbackTestCycle(
private val rubricProviderClassNames: List<String>,
private val rubricProviderClassName: String,
private val submission: Submission,
private val classLoader: RuntimeClassLoaderImpl,
private val notes: List<String>,
) : TestCycle {
override fun getRubricProviderClassNames(): List<String> = rubricProviderClassNames
override fun getRubricProviderName(): String = rubricProviderClassName
override fun getClassLoader(): RuntimeClassLoaderImpl = classLoader
override fun getSubmission(): Submission = submission
override fun getTestsSucceededCount(): Int = -1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
package org.sourcegrade.jagr.core.testing

import org.apache.logging.log4j.Logger
import org.sourcegrade.jagr.api.rubric.RubricForSubmission
import org.sourcegrade.jagr.api.rubric.RubricProvider
import org.sourcegrade.jagr.api.rubric.TestForSubmission
import org.sourcegrade.jagr.core.compiler.graderInfo
Expand All @@ -46,17 +45,10 @@ class GraderJarImpl(

override val configuration = RubricConfigurationImpl()

/**
* A map of assignments ids to classes of rubric providers (in the base classloader).
*
* Classes in this map are guaranteed to have an accessible no-args constructor.
*/
override val rubricProviders: Map<String, List<String>>

/**
* A map of assignment ids to JUnit test classes
*/
override val testProviders: Map<String, List<String>>
override val testClassNames: List<String>

private val graderFiles = info.graderFiles.toSet()
private val solutionFiles = info.solutionFiles.toSet()
Expand All @@ -79,49 +71,55 @@ class GraderJarImpl(
error("Grader ${info.name} file $fileName is not declared in the grader-info.json")
}
}
val rubricProviders: MutableMap<String, MutableList<String>> = mutableMapOf()
val testProviders: MutableMap<String, MutableList<String>> = mutableMapOf()
val testClasses = mutableListOf<String>()
val baseClassLoader = RuntimeClassLoaderImpl(container.runtimeResources + libraries)
var foundRubricProvider = false
for (className in container.runtimeResources.classes.keys) {
val clazz = baseClassLoader.loadClass(className)
rubricProviders.putIfRubric(clazz)
testProviders.putIfTest(clazz)
if (clazz.name == info.rubricProviderName) {
checkRubricProvider(clazz)
foundRubricProvider = true
}
testClasses.addIfTest(clazz)
}
if (!foundRubricProvider) {
error("Grader ${info.name} is missing rubric provider class ${info.rubricProviderName}")
}
logger.info(
"Grader ${info.name} discovered " +
"${rubricProviders.size} rubric provider${if (rubricProviders.size == 1) "" else "s"} and " +
"${testProviders.size} test provider${if (testProviders.size == 1) "" else "s"}"
"rubric provider ${info.rubricProviderName} and " +
"${testClasses.size} test class${if (testClasses.size == 1) "" else "es"}"
)
this.rubricProviders = rubricProviders
this.testProviders = testProviders
this.testClassNames = testClasses
}

private fun MutableMap<String, MutableList<String>>.putIfRubric(clazz: Class<*>) {
val annotation = clazz.getAnnotation(RubricForSubmission::class.java) ?: return
private fun checkRubricProvider(clazz: Class<*>) {
val asRubricProvider = try {
clazz.asSubclass(RubricProvider::class.java)
} catch (e: ClassCastException) {
logger.error(
"Grader ${info.name} class ${clazz.name} annotated with @RubricForSubmission " +
"does not implement RubricProvider! Ignoring..."
)
return
throw IllegalStateException("Grader ${info.name} class declared as rubric provider does not implement RubricProvider", e)
}

val rubricProvider = try {
checkNotNull(asRubricProvider.getConstructor().newInstance())
} catch (e: NoSuchMethodException) {
logger.error("Grader ${info.name} rubric provider ${clazz.name} must have an accessible no-args constructor!")
return
throw IllegalStateException("Grader ${info.name} rubric provider ${clazz.name} must have an accessible no-args constructor!", e)
}
rubricProvider.configure(configuration)
logger.debug("Grader ${info.name} discovered rubric provider ${clazz.name} for assignment ${annotation.value}")
computeIfAbsent(annotation.value) { mutableListOf() }.add(asRubricProvider.name)
}

private fun MutableMap<String, MutableList<String>>.putIfTest(clazz: Class<*>) {
@Suppress("DEPRECATION")
private fun MutableList<String>.addIfTest(clazz: Class<*>) {
val annotation = clazz.getAnnotation(TestForSubmission::class.java) ?: return
computeIfAbsent(annotation.value) { mutableListOf() }.add(clazz.name)
add(clazz.name)
if (annotation.value.isNotBlank() && annotation.value != clazz.name) {
logger.warn(
"Grader ${info.name} test class ${clazz.name} " +
"has a non-blank value ${annotation.value} in @TestForSubmission and it does not match " +
"the grader's assignmentId ${info.assignmentId}"
)
logger.warn("Providing a value to @TestForSubmission is deprecated and will be removed in a future version")
}
logger.debug("Grader ${info.name} discovered test provider ${clazz.name} for assignment ${annotation.value}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,10 @@ class JavaRuntimeTester @Inject constructor(
override fun createTestCycle(grader: GraderJarImpl, submission: Submission): TestCycle? {
if (submission !is JavaSubmission) return null
val info = submission.submissionInfo
val rubricProviders = grader.rubricProviders[info.assignmentId]
if (rubricProviders == null) {
if (info.assignmentId != grader.info.assignmentId) {
logger.warn(
"Submission $info does not have any applicable rubric providers! " +
"assignmentId: ${info.assignmentId}, available: ${grader.rubricProviders.keys}"
"Submission $info assignmentId '${info.assignmentId}' != " +
"grader's ${grader.info.name} assignmentId '${grader.info.assignmentId}'"
)
return null
}
Expand All @@ -53,11 +52,11 @@ class JavaRuntimeTester @Inject constructor(
submission.libraries +
grader.containerWithoutSolution.runtimeResources
)
val testCycle = JavaTestCycle(rubricProviders, submission, classLoader)
grader.testProviders[info.assignmentId]
?.map { DiscoverySelectors.selectClass(classLoader.loadClass(it)) }
?.runJUnit(testCycle)
?.also(testCycle::setJUnitResult)
val testCycle = JavaTestCycle(grader.info.rubricProviderName, submission, classLoader)
grader.testClassNames
.map { DiscoverySelectors.selectClass(classLoader.loadClass(it)) }
.runJUnit(testCycle)
.also(testCycle::setJUnitResult)
return testCycle
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,16 @@ import org.sourcegrade.jagr.launcher.io.get
import org.sourcegrade.jagr.launcher.io.keyOf
import org.sourcegrade.jagr.launcher.io.openScope
import org.sourcegrade.jagr.launcher.io.read
import org.sourcegrade.jagr.launcher.io.readList
import org.sourcegrade.jagr.launcher.io.writeList

data class JavaTestCycle(
private val rubricProviderClassNames: List<String>,
private val rubricProviderName: String,
private val submission: JavaSubmission,
private val classLoader: RuntimeClassLoaderImpl,
private var testsSucceededCount: Int = -1,
private var testsStartedCount: Int = -1,
) : TestCycle {
private var jUnitResult: TestCycle.JUnitResult? = null
override fun getRubricProviderClassNames(): List<String> = rubricProviderClassNames
override fun getRubricProviderName(): String = rubricProviderName
override fun getClassLoader(): RuntimeClassLoaderImpl = classLoader
override fun getSubmission(): JavaSubmission = submission
override fun getTestsSucceededCount(): Int = testsSucceededCount
Expand All @@ -56,7 +54,7 @@ data class JavaTestCycle(

companion object Factory : SerializerFactory<JavaTestCycle> {
override fun read(scope: SerializationScope.Input) = JavaTestCycle(
scope.readList(),
scope.input.readUTF(),
scope[Submission::class] as JavaSubmission,
scope.openScope {
proxy(keyOf(RuntimeResources::class), RuntimeResources.base)
Expand All @@ -67,7 +65,7 @@ data class JavaTestCycle(
)

override fun write(obj: JavaTestCycle, scope: SerializationScope.Output) {
scope.writeList(obj.rubricProviderClassNames)
scope.output.writeUTF(obj.rubricProviderName)
scope.output.writeInt(obj.testsSucceededCount)
scope.output.writeInt(obj.testsStartedCount)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,15 @@ class RuntimeGraderImpl @Inject constructor(
.fold(emptyMap()) { a, b -> a + b }
}

private fun TestCycle.collectResults(): Map<GradedRubric, String> {
val result: MutableMap<GradedRubric, String> = mutableMapOf()
for (rubricProviderName in rubricProviderClassNames) {
val rubricProvider = try {
// rubric provider must first be loaded again together with submission classes
classLoader.loadClass(rubricProviderName).getConstructor().newInstance() as RubricProvider
} catch (e: Throwable) {
logger.error("Failed to initialize rubricProvider $rubricProviderName for $submission", e)
continue
}
val exportFileName = rubricProvider.getOutputFileName(submission) ?: submission.info.toString()
result[rubricProvider.rubric.grade(this)] = exportFileName
private fun TestCycle.collectResults(): Pair<GradedRubric, String>? {
val rubricProvider = try {
// rubric provider must first be loaded again together with submission classes
classLoader.loadClass(rubricProviderName).getConstructor().newInstance() as RubricProvider
} catch (e: Throwable) {
logger.error("Failed to initialize rubric provider $rubricProviderName for $submission", e)
return null
}
return result
val exportFileName = rubricProvider.getOutputFileName(submission) ?: submission.info.toString()
return rubricProvider.rubric.grade(this) to exportFileName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
import java.lang.annotation.Target;

/**
* This annotation is used to mark a class as a rubric provider for a submission.
* This annotation is used to mark a class as a {@link RubricProvider} for a submission.
*
* @deprecated This annotation is no longer used by Jagr as the class name is provided via the generated grader-info.json
*/
@Deprecated(forRemoval = true)
@ApiStatus.NonExtendable
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
* The assignment id of the submission.
*
* @return The assignment id of the submission
* @deprecated No longer read by Jagr, the assignmentId is provided via the generated grader-info.json
*/
String value();
@Deprecated(forRemoval = true)
String value() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
public interface TestCycle {

/**
* The class names of every rubric provider in this test cycle.
* The class name of the rubric provider in this test cycle.
*
* @return An immutable set of class names
* @return The class name of the rubric provider in this test cycle
*/
List<String> getRubricProviderClassNames();
String getRubricProviderName();

/**
* Every test cycle uses a unique {@link ClassLoader} that loads the grader jar's classes and the {@link Submission}'s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ abstract class GraderConfiguration(
.convention(listOf(name))

abstract val graderName: Property<String>
abstract val rubricProviderName: Property<String>
val parentConfiguration: Property<GraderConfiguration> = project.objects.property()

private val submissionConfigurationConvention = parentConfiguration.flatMap { it.submissionConfiguration }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package org.sourcegrade.jagr.gradle.task.grader
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.get
Expand All @@ -26,6 +28,10 @@ import java.io.File
@Suppress("LeakingThis")
abstract class GraderWriteInfoTask : DefaultTask(), GraderTask {

@get:Input
@get:Optional
abstract val rubricProviderName: Property<String>

@get:Input
val graderFiles: ListProperty<String> = project.objects.listProperty<String>().value(
configurationName.map { configuration ->
Expand Down Expand Up @@ -61,12 +67,26 @@ abstract class GraderWriteInfoTask : DefaultTask(), GraderTask {
return result
}

private fun GraderConfiguration.getRubricProviderNameRecursive(): String {
return if (rubricProviderName.isPresent) {
rubricProviderName.get()
} else if (parentConfiguration.isPresent) {
parentConfiguration.get().getRubricProviderNameRecursive()
} else {
throw GradleException(
"No rubricProviderName defined for grader configuration ${configurationName.get()} or its parents"
)
}
}

@TaskAction
fun runTask() {
val jagr = project.extensions.getByType<JagrExtension>()
val graderInfo = GraderInfo(
assignmentId.get(),
Jagr.version,
graderName.get(),
rubricProviderName.getOrElse(jagr.graders[configurationName.get()].getRubricProviderNameRecursive()),
listOf(
SourceSetInfo("grader", graderFiles.get()),
SourceSetInfo("solution", solutionFiles.get())
Expand All @@ -82,7 +102,7 @@ abstract class GraderWriteInfoTask : DefaultTask(), GraderTask {
override fun determineTaskName(name: String) = "${name}WriteGraderInfo"
override fun configureTask(task: GraderWriteInfoTask, project: Project, configuration: GraderConfiguration) {
task.description = "Runs the ${task.sourceSetNames.get()} grader"
task.assignmentId.set(project.extensions.getByType<JagrExtension>().assignmentId)
task.rubricProviderName.set(configuration.rubricProviderName)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,23 @@ data class GraderInfo(
val assignmentId: String,
val jagrVersion: String,
val name: String,
val rubricProviderName: String,
val sourceSets: List<SourceSetInfo>,
) {
companion object Factory : SerializerFactory<GraderInfo> {
override fun read(scope: SerializationScope.Input) = GraderInfo(
scope.input.readUTF(),
scope.input.readUTF(),
scope.input.readUTF(),
scope.input.readUTF(),
scope.readList(),
)

override fun write(obj: GraderInfo, scope: SerializationScope.Output) {
scope.output.writeUTF(obj.assignmentId)
scope.output.writeUTF(obj.jagrVersion)
scope.output.writeUTF(obj.name)
scope.output.writeUTF(obj.rubricProviderName)
scope.writeList(obj.sourceSets)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,5 @@ import org.sourcegrade.jagr.api.testing.RubricConfiguration
interface GraderJar {
val info: GraderInfo
val configuration: RubricConfiguration
val rubricProviders: Map<String, List<String>>
val testProviders: Map<String, List<String>>
val testClassNames: List<String>
}