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

Exporter for sourcegrade/lab #303

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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 @@ -34,6 +34,7 @@ import org.sourcegrade.jagr.core.executor.GradingQueueFactoryImpl
import org.sourcegrade.jagr.core.executor.TimeoutHandler
import org.sourcegrade.jagr.core.export.rubric.BasicHTMLExporter
import org.sourcegrade.jagr.core.export.rubric.GermanCSVExporter
import org.sourcegrade.jagr.core.export.rubric.LabExporter
import org.sourcegrade.jagr.core.export.rubric.MoodleJSONExporter
import org.sourcegrade.jagr.core.export.submission.EclipseSubmissionExporter
import org.sourcegrade.jagr.core.export.submission.GradleSubmissionExporter
Expand Down Expand Up @@ -78,6 +79,7 @@ class CommonModule(private val configuration: LaunchConfiguration) : AbstractMod
bind(GradedRubricExporter.CSV::class.java).to(GermanCSVExporter::class.java)
bind(GradedRubricExporter.HTML::class.java).to(BasicHTMLExporter::class.java)
bind(GradedRubricExporter.Moodle::class.java).to(MoodleJSONExporter::class.java)
bind(GradedRubricExporter.Lab::class.java).to(LabExporter::class.java)
bind(Grader.Factory::class.java).to(GraderFactoryImpl::class.java)
bind(GradeResult.Factory::class.java).to(GradeResultFactoryImpl::class.java)
bind(GradingQueue.Factory::class.java).to(GradingQueueFactoryImpl::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package org.sourcegrade.jagr.core.export.rubric

import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.sourcegrade.jagr.api.rubric.GradedCriterion
import org.sourcegrade.jagr.api.rubric.GradedRubric
import org.sourcegrade.jagr.api.rubric.Grader
import org.sourcegrade.jagr.api.rubric.JUnitTestRef
import org.sourcegrade.jagr.core.rubric.JUnitTestRefFactoryImpl
import org.sourcegrade.jagr.core.rubric.grader.DescendingPriorityGrader
import org.sourcegrade.jagr.core.rubric.grader.TestAwareGraderImpl
import org.sourcegrade.jagr.core.testing.JavaSubmission
import org.sourcegrade.jagr.launcher.io.GradedRubricExporter
import org.sourcegrade.jagr.launcher.io.Resource
import org.sourcegrade.jagr.launcher.io.SubmissionInfo
import org.sourcegrade.jagr.launcher.io.buildResource

class LabExporter : GradedRubricExporter.Lab {

override fun export(gradedRubric: GradedRubric): Resource {
val jUnitResult = gradedRubric.testCycle.jUnitResult

if (jUnitResult != null) {
val testPlan = jUnitResult.testPlan
val statusListener = jUnitResult.statusListener

// Gather detailed test results
val testResults = testPlan.roots.flatMap { root ->
// Collect detailed information about each test
testPlan.getDescendants(root).mapNotNull { testIdentifier ->
val testExecutionResult = statusListener.testResults[testIdentifier]

// If the test has a result, collect the information
testExecutionResult?.let {
TestResult(
id = testIdentifier.uniqueId,
name = testIdentifier.displayName,
type = testIdentifier.type.toString(),
status = testExecutionResult.status.toString(),
// duration = Duration.between(it.startTime, it.endTime).toMillis(),
message = testExecutionResult.throwable.orElse(null)?.message,
stackTrace = testExecutionResult.throwable.orElse(null)?.stackTraceToString(),
)
}
}
}

// Get all relevant tests for a grader
fun getRelevantTests(grader: Grader): List<String> {
return when (grader) {
is TestAwareGraderImpl -> {
val testRefs: MutableSet<JUnitTestRef> = mutableSetOf()
testRefs.addAll(grader.requirePass.keys)
testRefs.addAll(grader.requireFail.keys)

testRefs.mapNotNull { ref ->
when (ref) {
is JUnitTestRefFactoryImpl.Default -> testPlan.roots.flatMap { testPlan.getDescendants(it) }.firstOrNull {
it.source.isPresent && it.source.orElse(null) == ref.testSource
}?.uniqueId
else -> null
}
}
}
is DescendingPriorityGrader -> grader.graders.flatMap { getRelevantTests(it) }
else -> emptyList()
}
}

// recursive function to get all criteria with children
fun getCriteria(criterion: GradedCriterion): Criterion {
val children = criterion.childCriteria.map { getCriteria(it) }
// gradedRubric.grade.comments
val relevantTests = children.flatMap { it.relevantTests ?: emptyList() }.toMutableSet()
if (criterion.criterion.grader != null) {
relevantTests.addAll(getRelevantTests(criterion.criterion.grader!!))
}
return Criterion(
name = criterion.criterion.shortDescription,
archivedPointsMin = criterion.grade.minPoints,
archivedPointsMax = criterion.grade.maxPoints,
message = criterion.grade.comments.joinToString("<br>") { "<p>$it</p>" },
relevantTests = relevantTests.toList(),
children = children,
)
}

// Serialize the results to JSON
val testResultsJson = LabRubric(
submissionInfo = (gradedRubric.testCycle.submission as JavaSubmission).submissionInfo,
totalPointsMin = gradedRubric.grade.minPoints,
totalPointsMax = gradedRubric.grade.maxPoints,
criteria = gradedRubric.childCriteria.map { getCriteria(it) },
tests = testResults,
)
val jsonString = Json.encodeToString(testResultsJson)

// Build the Resource with the JSON string
return buildResource {
name = "${gradedRubric.testCycle.submission.info}.json"
outputStream.bufferedWriter().use { it.write(jsonString) }
}
} else {
throw IllegalArgumentException("No JUnitResult present in the test cycle.")
}
}

@Serializable
data class TestResult(
val id: String,
val name: String,
val type: String,
val status: String,
// val duration: Long,
val message: String? = null,
val stackTrace: String? = null,
val children: List<TestResult> = emptyList(),
)

@Serializable
data class Criterion(
val name: String,
val archivedPointsMin: Int,
val archivedPointsMax: Int,
val message: String? = null,
val relevantTests: List<String>? = emptyList(),
val children: List<Criterion> = emptyList(),
)

@Serializable
data class LabRubric(
val submissionInfo: SubmissionInfo,
val totalPointsMin: Int,
val totalPointsMax: Int,
val criteria: List<Criterion> = emptyList(),
val tests: List<TestResult> = emptyList(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class CriterionImpl(
override fun getParentCriterion(): Criterion? = parentCriterionKt
override fun getPeers(): List<CriterionImpl> = peersKt
override fun getChildCriteria(): List<CriterionImpl> = childCriteria
override fun getGrader(): Grader? = grader
override fun grade(testCycle: TestCycle): GradedCriterion {
val graderResult = GradeResult.clamped(
grader?.grade(testCycle, this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class JUnitTestRefFactoryImpl @Inject constructor(
TestExecutionResult.aborted(NoOpFailedError())
}

class Default(private val testSource: TestSource) : JUnitTestRef {
class Default(val testSource: TestSource) : JUnitTestRef {
inner class TestNotFoundError : AssertionFailedError("Test result not found")

override operator fun get(testResults: Map<TestIdentifier, TestExecutionResult>): TestExecutionResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import org.sourcegrade.jagr.core.rubric.GradeResultImpl

class DescendingPriorityGrader(
private val logger: Logger,
private vararg val graders: Grader,
vararg val graders: Grader,
) : Grader {

override fun grade(testCycle: TestCycle, criterion: Criterion): GradeResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import org.sourcegrade.jagr.core.rubric.message
class TestAwareGraderImpl(
private val graderPassed: Grader,
private val graderFailed: Grader,
private val requirePass: Map<JUnitTestRef, String?>,
private val requireFail: Map<JUnitTestRef, String?>,
val requirePass: Map<JUnitTestRef, String?>,
val requireFail: Map<JUnitTestRef, String?>,
private val commentIfFailed: String?,
) : Grader {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ static Builder builder() {
*/
@Nullable Criterion getParentCriterion();

/**
* The {@link Grader} that will be used to calculate the points for the criterion.
*
* @return The {@link Grader} to use for this {@link Criterion}
*/
@Nullable Grader getGrader();

/**
* The peers of this criterion.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ interface GradedRubricExporter {
interface CSV : GradedRubricExporter
interface HTML : GradedRubricExporter
interface Moodle : GradedRubricExporter
interface Lab : GradedRubricExporter
}
2 changes: 2 additions & 0 deletions src/main/kotlin/org/sourcegrade/jagr/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.github.ajalt.clikt.parameters.types.choice
import org.sourcegrade.jagr.launcher.env.Environment
import org.sourcegrade.jagr.launcher.env.Jagr
import org.sourcegrade.jagr.launcher.env.logger
import kotlin.system.exitProcess

fun main(vararg args: String) {
try {
Expand All @@ -35,6 +36,7 @@ fun main(vararg args: String) {
Jagr.logger.error("A fatal error occurred", e)
throw e
}
exitProcess(0)
}

class MainCommand : CliktCommand() {
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/org/sourcegrade/jagr/StandardGrading.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ class StandardGrading(
private val rubricsFile = File(config.dir.rubrics).ensure(jagr.logger)!!
private val csvDir = checkNotNull(rubricsFile.resolve("csv").ensure(jagr.logger)) { "rubrics/csv directory" }
private val moodleDir = checkNotNull(rubricsFile.resolve("moodle").ensure(jagr.logger)) { "rubrics/moodle directory" }
private val labDir = checkNotNull(rubricsFile.resolve("lab").ensure(jagr.logger)) { "rubrics/lab directory" }
private val csvExporter = jagr.injector.getInstance(GradedRubricExporter.CSV::class.java)
private val moodleExporter = jagr.injector.getInstance(GradedRubricExporter.Moodle::class.java)
private val labExporter = jagr.injector.getInstance(GradedRubricExporter.Lab::class.java)

fun grade(noExport: Boolean, exportOnly: Boolean) = runBlocking {
jagr.logger.info("Starting Jagr v${Jagr.version}")
Expand Down Expand Up @@ -123,6 +125,7 @@ class StandardGrading(
for ((gradedRubric, _) in result.rubrics) {
csvExporter.exportSafe(gradedRubric, csvDir)
moodleExporter.exportSafe(gradedRubric, moodleDir)
labExporter.exportSafe(gradedRubric, labDir)
}
}

Expand Down