Skip to content

Commit

Permalink
Show evaluation metrics in submissions overview [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
erikhofer committed Oct 8, 2019
1 parent 8515865 commit e5b4075
Show file tree
Hide file tree
Showing 14 changed files with 152 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,23 +121,18 @@ class AssignmentController : BaseController() {
): String {
val assignment = assignmentService.findAssignment(assignmentId)
val submissions = submissionService.findSubmissionsOfAssignment(assignmentId)
// map of Answer#id to Evaluation
val evaluations = submissions
// map of Answer#id to EvaluationViewModel
val evaluationViewModels = submissions
.map { submission -> submission.answers.map { it.id } }
.map { evaluationService.getLatestEvaluations(it) }
.map { evaluationService.getLatestEvaluations(it).mapValues {
entry -> entry.value.map {e -> EvaluationViewModel.create(e, evaluationService) } }
}
.flatMap { it.toList() }
.toMap()
// map of EvaluationResult#id to ResultType
val resultTypes = evaluations.filterValues { it.isPresent }
.mapValues { it.value.get().results }
.flatMap { it.value }
.map { it.id to evaluationService.getResultType(it) }
.toMap()

model.addAttribute("resultTypes", resultTypes)
model.addAttribute("assignment", assignment)
model.addAttribute("submissions", submissions)
model.addAttribute("evaluations", evaluations)
model.addAttribute("evaluationViewModels", evaluationViewModels)
return "submissions"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,10 @@ class EvaluationController : BaseController() {
fun getEvaluation(@PathVariable("evaluationId") evaluationId: UUID, model: Model): String {
val evaluation = evaluationService.getEvaluation(evaluationId)
// TODO authorization
val resultTemplates = mutableMapOf<UUID, String>()
val resultContents = mutableMapOf<UUID, Any>()
evaluation.results.forEach {
if (it.error) {
resultContents[it.id] = String(it.content)
resultTemplates[it.id] = "error"
} else {
try {
resultContents[it.id] = evaluationService.getEvaluationRunner(it.runnerName).parseResultContent(it.content)
resultTemplates[it.id] = it.runnerName
} catch (e: Exception) {
log.error(e.message)
resultContents[it.id] = "Error while displaying result"
resultTemplates[it.id] = "error"
}
}
}
val viewModel = EvaluationViewModel.create(evaluation, evaluationService)
model.addAttribute("evaluation", evaluation)
model.addAttribute("resultTemplates", resultTemplates)
model.addAttribute("resultContents", resultContents)
model.addAttribute("resultTemplates", viewModel.resultTemplates)
model.addAttribute("resultContents", viewModel.resultContents)
return "evaluation"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.code_freak.codefreak.frontend

import de.code_freak.codefreak.entity.Evaluation
import de.code_freak.codefreak.service.evaluation.EvaluationService
import org.slf4j.LoggerFactory
import java.util.UUID

data class EvaluationViewModel(val evaluation: Evaluation,
val resultContents: Map<UUID, Any>,
val resultTemplates: Map<UUID, String>) {

companion object {
private val log = LoggerFactory.getLogger(this::class.java)
fun create(evaluation: Evaluation, evaluationService: EvaluationService): EvaluationViewModel {
val resultTemplates = mutableMapOf<UUID, String>()
val resultContents = mutableMapOf<UUID, Any>()
evaluation.results.forEach {
if (it.error) {
resultContents[it.id] = String(it.content)
resultTemplates[it.id] = "error"
} else {
try {
resultContents[it.id] = evaluationService.getEvaluationRunner(it.runnerName).parseResultContent(it.content)
resultTemplates[it.id] = it.runnerName
} catch (e: Exception) {
log.error(e.message)
resultContents[it.id] = "Error while displaying result"
resultTemplates[it.id] = "error"
}
}
}
return EvaluationViewModel(evaluation, resultContents, resultTemplates)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ interface EvaluationRunner {
fun getName(): String
fun run(answer: Answer, options: Map<String, Any>): String
fun parseResultContent(content: ByteArray): Any
fun getResultType(parsedResultContent: Any): ResultType

fun <T : Any> Map<String, Any>.get(key: String, type: KClass<T>): T? =
get(key)?.let { type.safeCast(it) ?: throw IllegalArgumentException("Option '$key' has invalid format") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,4 @@ class EvaluationService : BaseService() {
fun getEvaluation(evaluationId: UUID): Evaluation {
return evaluationRepository.findById(evaluationId).orElseThrow { EntityNotFoundException("Evaluation not found") }
}

fun getResultType(result: EvaluationResult): ResultType {
val runner = getEvaluationRunner(result.runnerName)
return runner.getResultType(
runner.parseResultContent(result.content)
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.code_freak.codefreak.entity.Answer
import de.code_freak.codefreak.service.ContainerService
import de.code_freak.codefreak.service.evaluation.EvaluationRunner
import de.code_freak.codefreak.service.evaluation.ResultType
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

Expand Down Expand Up @@ -74,13 +73,4 @@ class CodeclimateRunner : EvaluationRunner {
}
}
}

override fun getResultType(parsedResultContent: Any): ResultType {
parsedResultContent as Content
return if (parsedResultContent.issues.isNotEmpty()) {
ResultType.FAILURE
} else {
ResultType.SUCCESS
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import de.code_freak.codefreak.entity.Answer
import de.code_freak.codefreak.service.ContainerService
import de.code_freak.codefreak.service.ExecResult
import de.code_freak.codefreak.service.evaluation.EvaluationRunner
import de.code_freak.codefreak.service.evaluation.ResultType
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.io.InputStream
Expand Down Expand Up @@ -43,15 +42,4 @@ class CommandLineRunner : EvaluationRunner {
override fun parseResultContent(content: ByteArray): Any {
return mapper.readValue(content, Array<Execution>::class.java)
}

override fun getResultType(parsedResultContent: Any): ResultType {
parsedResultContent as Array<*>
parsedResultContent.map {
it as Execution
if (it.result.exitCode != 0L) {
return ResultType.FAILURE
}
}
return ResultType.SUCCESS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package de.code_freak.codefreak.service.evaluation.runner

import com.fasterxml.jackson.databind.ObjectMapper
import de.code_freak.codefreak.entity.Answer
import de.code_freak.codefreak.service.evaluation.ResultType
import de.code_freak.codefreak.util.TarUtil
import de.code_freak.codefreak.util.withTrailingSlash
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
Expand Down Expand Up @@ -58,26 +57,4 @@ class JUnitRunner : CommandLineRunner() {
val testSuites = results.xmlReports.map { JUnitMarshalling.unmarshalTestSuite(ByteArrayInputStream(it)) }
return RenderResults(results.executions, testSuites)
}

override fun getResultType(parsedResultContent: Any): ResultType {
parsedResultContent as RenderResults
try {
parsedResultContent.executions.map {
if (it.result.exitCode != 0L) {
return ResultType.FAILURE
}
}
parsedResultContent.testSuites.map {
if (it.errors > 0) {
return ResultType.ERROR
}
if (it.failures > 0) {
return ResultType.FAILURE
}
}
return ResultType.SUCCESS
} catch (e: Exception) {
return ResultType.ERROR
}
}
}
42 changes: 42 additions & 0 deletions src/main/resources/templates/evaluation-summary/codeclimate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<span
th:with="numIssues=${#lists.size(content.issues)}"
th:title="|Error determining result of ${result.runnerName} runner|" data-toggle="tooltip">
<th:block th:text="${numIssues}"></th:block>
</span>
<h3 class="card-title">Code Quality</h3>
<div class="alert alert-success lead" th:if="${#lists.size(content.issues) == 0}">
<i class="far fa-check-circle text-success"></i> No code-quality issues detected!
</div>
<th:block th:each="issue : ${content.issues}">
<h4 class="mt-3" th:text="${issue.description}"></h4>
<p>
Severity:
<th:block th:switch="${issue.severity}">
<span th:case="'info'" class="badge badge-info">Info</span>
<span th:case="'minor'" class="badge badge-secondary">Minor</span>
<span th:case="'major'" class="badge badge-warning">Major</span>
<span th:case="'critical'" class="badge badge-danger">Critical</span>
<span th:case="'blocker'" class="badge badge-danger">Blocker</span>
</th:block>
Categories:
<th:block th:each="category : ${issue.categories}">
<span th:text="${category}" class="badge badge-secondary"></span>
</th:block>
<span class="text-muted">by <span th:text="${issue.engine_name}"></span></span>
<br>
File: <code th:utext="${issue.location.path}"></code> at line
<span th:text="${issue.location.lines.begin}"></span>
<span th:if="${issue.location.lines.begin != issue.location.lines.end}" th:text="${'-' + issue.location.lines.end}"></span>
</p>
<th:block th:if="${issue.content}" th:with="id=${'details-'+T(java.util.UUID).randomUUID()}">
<button class="btn btn-light" type="button" data-toggle="collapse" th:attr="data-target=${'#' + id}">Show more information</button>
<div class="collapse mt-3" th:id="${id}">
<div class="card card-body" th:utext="${@markdown.html(issue.content.body)}"></div>
</div>
</th:block>
</th:block>
</body>
</html>
17 changes: 17 additions & 0 deletions src/main/resources/templates/evaluation-summary/commandline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="card-title">Command Line</h3>
<div class="mb-3" th:each="execution : ${content}">
<h4>
<th:block th:switch="${execution.result.exitCode}">
<span th:case="0" class="badge badge-success">Success</span>
<span th:case="-1" class="badge badge-info">Skipped</span>
<span th:case="*" class="badge badge-danger">Failure</span>
</th:block>
<code th:text="${execution.command}"></code>
</h4>
<pre th:text="${execution.result.output}"></pre>
</div>
</body>
</html>
6 changes: 6 additions & 0 deletions src/main/resources/templates/evaluation-summary/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<i class="fas fa-exclamation-circle text-danger" th:title="|Error determining result of ${result.runnerName} runner|" data-toggle="tooltip"></i>
</body>
</html>
34 changes: 34 additions & 0 deletions src/main/resources/templates/evaluation-summary/junit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="card-title">Unit Tests</h3>
<th:block th:with="error = ${content.executions.?[result.exitCode == -1].size() > 0}">
<span th:unless="${error}" th:with="" th:each="testSuite : ${content.testSuites}" title="Unit tests could not be executed" data-toggle="tooltip">
<h4>
<span th:text="${testSuite.name}"></span>
<span th:text="${testSuite.tests - testSuite.skipped - testSuite.failures - testSuite.errors} + ' Passed'" class="badge badge-success"></span>
<span th:if="${testSuite.failures > 0}" th:text="${testSuite.failures} + ' Failed'" class="badge badge-danger"></span>
<span th:if="${testSuite.errors > 0}" th:text="${testSuite.errors} + ' Errored'" class="badge badge-danger"></span>
<span th:if="${testSuite.skipped > 0}" th:text="${testSuite.skipped} + ' Skipped'" class="badge badge-info"></span>
</h4>
<ul class="list-group">
<li class="list-group-item" th:each="testCase : ${testSuite.testCases}">
<th:block th:switch="true">
<i th:case="${testCase.skipped != null}" class="fas fa-forward text-info"></i>
<i th:case="${testCase.errors != null}" class="fas fa-exclamation-circle text-danger"></i>
<i th:case="${testCase.failures != null}" class="fas fa-times-circle text-danger"></i>
<i th:case="*" class="far fa-check-circle text-success"></i>
</th:block>
<span th:text="${testCase.name}"></span>
<pre class="alert alert-danger mt-2 mb-1" th:each="failure : ${testCase.failures}"
th:text="${failure.message.startsWith(failure.type+': ') ? #strings.substring(failure.message, failure.type.length()+2) : failure.message}">
</pre>
</li>
</ul>
</span>
<span th:if="${error}" title="Unit tests could not be executed" data-toggle="tooltip">
N/A <i class="fas fa-times-circle text-danger"></i>
</span>
</th:block>
</body>
</html>
16 changes: 9 additions & 7 deletions src/main/resources/templates/submissions.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,23 @@
<tbody>
<tr th:each="submission : ${submissions}">
<td th:text="${submission.user.username}"></td>
<td th:each="task : ${assignment.tasks}" th:with="answer=${submission.getAnswer(task.id)}, evaluation=${evaluations.get(answer.id)}">
<td th:each="task : ${assignment.tasks}" th:with="answer=${submission.getAnswer(task.id)}, evaluationViewModel=${evaluationViewModels.get(answer.id)}">
<span th:if="${answer == null}" class="text-muted">no answer</span>
<th:block th:if="${answer != null}" th:with="btnClass='btn-sm',url=${@urls.get(answer) + '/source'}" th:insert="fragments/archive-download" />
<a th:if="${answer != null}"
class="btn btn-primary btn-sm"
th:classappend="${evaluation.isPresent() ? '' : 'disabled'}"
th:href="${evaluation.isPresent() ? @urls.get(evaluation.get()) : '#'}">
th:classappend="${evaluationViewModel.isPresent() ? '' : 'disabled'}"
th:href="${evaluationViewModel.isPresent() ? @urls.get(evaluationViewModel.get().evaluation) : '#'}">
<i class="fas fa-poll"></i>
</a>
<div class="d-inline-block" th:if="${evaluation.isPresent()}">
<th:block th:each="result : ${evaluation.get().results}" th:switch="${resultTypes.get(result.id).name()}">
<i th:case="'SUCCESS'" class="fas fa-check-circle text-success" th:title="${result.runnerName}" data-toggle="tooltip"></i>
<div class="d-inline-block" th:if="${evaluationViewModel.isPresent()}">
<th:block th:each="result : ${evaluationViewModel.get().evaluation.results}"
th:with="content=${evaluationViewModel.get().resultContents[result.id]}"
th:insert="${'evaluation-summary/' + evaluationViewModel.get().resultTemplates[result.id]}">
<!--<i th:case="'SUCCESS'" class="fas fa-check-circle text-success" th:title="${result.runnerName}" data-toggle="tooltip"></i>
<i th:case="'FAILURE'" class="fas fa-times-circle text-danger" th:title="${result.runnerName}" data-toggle="tooltip"></i>
<i th:case="'ERROR'" class="fas fa-exclamation-circle text-danger" th:title="${result.runnerName}" data-toggle="tooltip"></i>
<i th:case="*" class="fas fa-question-circle text-info" th:title="${result.runnerName}" data-toggle="tooltip"></i>
<i th:case="*" class="fas fa-question-circle text-info" th:title="${result.runnerName}" data-toggle="tooltip"></i>-->
</th:block>
</div>
</td>
Expand Down

0 comments on commit e5b4075

Please sign in to comment.