diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb
index ebf9d4da..968951a8 100644
--- a/app/assets/javascripts/editor.js.erb
+++ b/app/assets/javascripts/editor.js.erb
@@ -703,7 +703,8 @@ $(function() {
panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text((result.score * result.weight).toFixed(2));
panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
panel.find('.row .col-sm-9').eq(2).text(result.message);
- panel.find('.row .col-sm-9').eq(3).find('a').attr('href', '#output-' + index);
+ if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(', '));
+ panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
};
var chunkBuffer = [{streamedResponse: true}];
diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss
index 631de920..ebabe3f7 100644
--- a/app/assets/stylesheets/statistics.css.scss
+++ b/app/assets/stylesheets/statistics.css.scss
@@ -26,3 +26,26 @@
margin-top: auto;
margin-bottom: auto;
}
+
+div.unit-test-result {
+ float: left;
+ margin-right: 10px;
+ width: 10px;
+ height: 10px;
+}
+
+div.positive-result {
+ border-radius: 50%;
+ background-color: #8efa00;
+ -webkit-box-shadow: 0px 0px 11px 1px rgba(44,222,0,1);
+ -moz-box-shadow: 0px 0px 11px 1px rgba(44,222,0,1);
+ box-shadow: 0px 0px 11px 1px rgba(44,222,0,1);
+}
+
+div.negative-result {
+ border-radius: 50%;
+ background-color: #ff2600;
+ -webkit-box-shadow: 0px 0px 11px 1px rgba(222,0,0,1);
+ -moz-box-shadow: 0px 0px 11px 1px rgba(222,0,0,1);
+ box-shadow: 0px 0px 11px 1px rgba(222,0,0,1);
+}
diff --git a/app/controllers/concerns/submission_scoring.rb b/app/controllers/concerns/submission_scoring.rb
index 081f0d01..c8dfb0b3 100644
--- a/app/controllers/concerns/submission_scoring.rb
+++ b/app/controllers/concerns/submission_scoring.rb
@@ -6,7 +6,11 @@ module SubmissionScoring
future = Concurrent::Future.execute do
assessor = Assessor.new(execution_environment: submission.execution_environment)
output = execute_test_file(file, submission)
- output.merge!(assessor.assess(output))
+ assessment = assessor.assess(output)
+ passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0))
+ testrun_output = passed ? nil : output[:stderr]
+ Testrun.new(submission: submission, file: file, passed: passed, output: testrun_output).save
+ output.merge!(assessment)
output.merge!(filename: file.name_with_extension, message: feedback_message(file, output[:score]), weight: file.weight)
end
future.value
@@ -27,9 +31,9 @@ module SubmissionScoring
def score_submission(submission)
outputs = collect_test_results(submission)
score = 0.0
- if not (outputs.nil? || outputs.empty?)
+ unless outputs.nil? || outputs.empty?
outputs.each do |output|
- if not output.nil?
+ unless output.nil?
score += output[:score] * output[:weight]
end
end
diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb
index 4220618d..48ec97d0 100644
--- a/app/models/code_ocean/file.rb
+++ b/app/models/code_ocean/file.rb
@@ -21,6 +21,7 @@ module CodeOcean
belongs_to :file_type
has_many :files
+ has_many :testruns
alias_method :descendants, :files
mount_uploader :native_file, FileUploader
diff --git a/app/models/submission.rb b/app/models/submission.rb
index 16ca74dc..323f1d58 100644
--- a/app/models/submission.rb
+++ b/app/models/submission.rb
@@ -7,6 +7,8 @@ class Submission < ActiveRecord::Base
belongs_to :exercise
+ has_many :testruns
+
delegate :execution_environment, to: :exercise
scope :final, -> { where(cause: 'submit') }
diff --git a/app/models/testrun.rb b/app/models/testrun.rb
index 316acfdc..a266edc3 100644
--- a/app/models/testrun.rb
+++ b/app/models/testrun.rb
@@ -1,4 +1,4 @@
class Testrun < ActiveRecord::Base
- belongs_to :file
+ belongs_to :file, class_name: 'CodeOcean::File'
belongs_to :submission
end
diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim
index 2bd640c1..da898ddd 100644
--- a/app/views/exercises/external_users/statistics.html.slim
+++ b/app/views/exercises/external_users/statistics.html.slim
@@ -38,7 +38,7 @@ h1 = "#{@exercise} (external user #{@external_user})"
table.table
thead
tr
- - ['.time', '.cause', '.score', '.time_difference'].each do |title|
+ - ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
th.header = t(title)
tbody
- deltas = submissions.map.with_index {|item, index| delta = item.created_at - submissions[index - 1].created_at if index > 0; if delta == nil or delta > 30*60 then 0 else delta end}
@@ -47,6 +47,12 @@ h1 = "#{@exercise} (external user #{@external_user})"
td.clickable = submission.created_at.strftime("%F %T")
td = submission.cause
td = submission.score
+ td
+ -submission.testruns.each do |run|
+ - if run.passed
+ .unit-test-result.positive-result
+ - else
+ .unit-test-result.negative-result
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
p = t('.addendum')
diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim
index 6ede6d68..728492b6 100644
--- a/app/views/exercises/implement.html.slim
+++ b/app/views/exercises/implement.html.slim
@@ -66,6 +66,7 @@
= row(label: '.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: '.feedback')
+ = row(label: '.error_messages')
= row(label: '.output', value: link_to(t('shared.show'), '#'))
#score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score)
h4
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 5d0bc8b1..f35a5442 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -222,6 +222,7 @@ de:
text: 'Ihr Browser unterstützt nicht alle Funktionalitäten, die %{application_name} benötigt. Bitte nutzen Sie einen modernen Browser, um %{application_name} zu besuchen.'
title: Ihr Browser wird nicht unterstützt!
default_feedback: Sehr gut. Alle Tests waren erfolgreich.
+ error_messages: Fehlermeldungen
feedback: Feedback
file: 'Test-Datei %{number} (%{filename})'
hint: Tipp
@@ -269,6 +270,7 @@ de:
time: Zeit
cause: Grund
score: Punktzahl
+ tests: Unit Tests
time_difference: 'Arbeitszeit bis hier*'
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
external_users:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 38c556f5..399e6c66 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -222,6 +222,7 @@ en:
text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.'
title: Your browser is not supported!
default_feedback: Well done. All tests have been passed.
+ error_messages: Error Messages
feedback: Feedback
file: 'Test File %{number} (%{filename})'
hint: Hint
@@ -269,6 +270,7 @@ en:
time: Time
cause: Cause
score: Score
+ tests: Unit Test Results
time_difference: 'Working Time until here*'
addendum: '* Deltas longer than 30 minutes are ignored.'
external_users:
diff --git a/lib/junit_adapter.rb b/lib/junit_adapter.rb
index 6a6061a0..87b70d84 100644
--- a/lib/junit_adapter.rb
+++ b/lib/junit_adapter.rb
@@ -2,6 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Tests run: (\d+)/
FAILURES_REGEXP = /Failures: (\d+)/
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/
+ ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)/
def self.framework_name
'JUnit'
@@ -13,7 +14,8 @@ class JunitAdapter < TestingFrameworkAdapter
else
count = COUNT_REGEXP.match(output[:stdout]).try(:captures).try(:first).try(:to_i) || 0
failed = FAILURES_REGEXP.match(output[:stdout]).try(:captures).try(:first).try(:to_i) || 0
- {count: count, failed: failed}
+ error_matches = ASSERTION_ERROR_REGEXP.match(output[:stdout]).try(:captures) || []
+ {count: count, failed: failed, error_messages: error_matches}
end
end
end
diff --git a/lib/py_unit_adapter.rb b/lib/py_unit_adapter.rb
index 0e80c593..68cdd200 100644
--- a/lib/py_unit_adapter.rb
+++ b/lib/py_unit_adapter.rb
@@ -1,6 +1,7 @@
class PyUnitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Ran (\d+) test/
FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/
+ ASSERTION_ERROR_REGEXP = /AssertionError:\s(.*)/
def self.framework_name
'PyUnit'
@@ -10,6 +11,7 @@ class PyUnitAdapter < TestingFrameworkAdapter
count = COUNT_REGEXP.match(output[:stderr]).captures.first.to_i
matches = FAILURES_REGEXP.match(output[:stderr])
failed = matches ? matches.captures.try(:first).to_i : 0
- {count: count, failed: failed}
+ error_matches = ASSERTION_ERROR_REGEXP.match(output[:stderr]).captures
+ {count: count, failed: failed, error_messages: error_matches}
end
end