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