diff --git a/Changelog.md b/Changelog.md
index 424707692d..66c9db8f00 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -5,6 +5,7 @@
### 🚨 Breaking changes
### ✨ New features and improvements
+- Added more download options to automated test result summaries (#7599)
### 🐛 Bug fixes
diff --git a/app/assets/stylesheets/common/_markus.scss b/app/assets/stylesheets/common/_markus.scss
index 7867e82139..7ff050a4dd 100644
--- a/app/assets/stylesheets/common/_markus.scss
+++ b/app/assets/stylesheets/common/_markus.scss
@@ -1530,3 +1530,76 @@ canvas {
.jcrop-centered {
display: inline-block;
}
+
+.feature-option {
+ margin: 0.5rem;
+}
+
+.feature-option input[type='radio'] {
+ display: none;
+}
+
+.feature-option label {
+ width: 100px;
+ background: linear-gradient(to top, $background-main, $background-support);
+ border: 1px solid $primary-three;
+ border-radius: $radius;
+ box-shadow: inset 0 -1px 0 $primary-two;
+ color: $line;
+ cursor: pointer;
+ display: inline-block;
+ font: 400 1em $fonts;
+ min-width: 150px;
+ outline: none;
+ padding: 0.5em 1.5em;
+ text-align: center;
+ transition: all $time-quick;
+}
+
+.feature-option input[type='radio']:checked + label {
+ background: linear-gradient(to bottom, $primary-one, $primary-three);
+ border: 1px solid $primary-one;
+ color: $background-main;
+ box-shadow: inset 0 1px 0 $primary-one;
+}
+
+.feature-option label:hover {
+ border-color: $primary-one;
+ box-shadow:
+ inset 0 -1px 0 $primary-two,
+ 0 1px 0 $background-main;
+}
+
+.feature-option input[type='radio']:disabled + label {
+ color: $line;
+ cursor: not-allowed;
+ background: linear-gradient(to top, $background-main, $disabled-area);
+ border-color: $disabled-area;
+ box-shadow: none;
+}
+
+.feature-option input[type='radio']:disabled + label:hover {
+ border-color: $disabled-area;
+}
+
+.feature-grid-wrap {
+ display: grid;
+ grid-template-columns: repeat(3, max-content);
+ column-gap: 0;
+}
+
+.feature-row {
+ display: grid;
+ grid-template-columns: subgrid;
+ grid-column: 1 / -1;
+ column-gap: inherit;
+ width: 150px;
+}
+
+.feature-submit-group {
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+ justify-content: right;
+ margin-top: 2rem;
+}
diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb
index 42cf411688..dd0f37a257 100644
--- a/app/controllers/assignments_controller.rb
+++ b/app/controllers/assignments_controller.rb
@@ -271,22 +271,54 @@ def summary
def download_test_results
@assignment = record
+
+ latest = params[:latest] == 'true'
+ student_run = params[:student_run] == 'true'
+ instructor_run = params[:instructor_run] == 'true'
+
+ test_results = SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: @assignment.test_groups,
+ latest:,
+ student_run:,
+ instructor_run:
+ )
+
respond_to do |format|
format.json do
- data = @assignment.summary_test_result_json
- filename = "#{@assignment.short_identifier}_test_results.json"
- send_data data,
- disposition: 'attachment',
- type: 'application/json',
- filename: filename
+ data = test_results.as_json
+
+ if latest
+ send_data(
+ data,
+ type: 'application/json',
+ disposition: 'attachment',
+ filename: "#{@assignment.short_identifier}_test_results.json"
+ )
+ else
+ zip_path = "tmp/#{@assignment.short_identifier}_test_results.zip"
+ Zip::File.open(zip_path, create: true) do |zip_file|
+ zip_file.get_output_stream('test_results.json') do |f|
+ f.write(data)
+ end
+ end
+
+ send_file(
+ zip_path,
+ disposition: 'attachment',
+ filename: "#{@assignment.short_identifier}_test_results.zip"
+ )
+ end
end
+
format.csv do
- data = @assignment.summary_test_result_csv
filename = "#{@assignment.short_identifier}_test_results.csv"
- send_data data,
- disposition: 'attachment',
- type: 'text/csv',
- filename: filename
+
+ send_data(
+ test_results.as_csv,
+ disposition: 'attachment',
+ type: 'text/csv',
+ filename: filename
+ )
end
end
end
diff --git a/app/helpers/summary_test_results_helper.rb b/app/helpers/summary_test_results_helper.rb
new file mode 100644
index 0000000000..63f4945d94
--- /dev/null
+++ b/app/helpers/summary_test_results_helper.rb
@@ -0,0 +1,97 @@
+module SummaryTestResultsHelper
+ class SummaryTestResults
+ class << self
+ def fetch(test_groups:, latest:, student_run:, instructor_run:)
+ query = base_query(latest:)
+
+ query = query.student_run if student_run && !instructor_run
+ query = query.instructor_run if !student_run && instructor_run
+
+ test_results = fetch_with_query(test_groups:, query:)
+
+ SummaryTestResult.new(test_results:)
+ end
+
+ private
+
+ def base_query(latest:)
+ if latest
+ TestRun.group('grouping_id').select('MAX(created_at) as test_runs_created_at', 'grouping_id')
+ else
+ TestRun.select('created_at as test_runs_created_at', 'grouping_id')
+ end
+ end
+
+ def fetch_with_query(test_groups:, query:)
+ latest_test_runs = TestRun
+ .joins(grouping: :group)
+ .joins("INNER JOIN (#{query.to_sql}) latest_test_run_by_grouping \
+ ON latest_test_run_by_grouping.grouping_id = test_runs.grouping_id \
+ AND latest_test_run_by_grouping.test_runs_created_at = test_runs.created_at")
+ .select('id', 'test_runs.grouping_id', 'groups.group_name')
+ .to_sql
+
+ test_groups.joins(test_group_results: :test_results)
+ .joins("INNER JOIN (#{latest_test_runs}) latest_test_runs \
+ ON test_group_results.test_run_id = latest_test_runs.id")
+ .select('test_groups.name',
+ 'test_groups.id as test_groups_id',
+ 'latest_test_runs.group_name',
+ 'test_results.name as test_result_name',
+ 'test_results.status',
+ 'test_results.marks_earned',
+ 'test_results.marks_total',
+ :output, :extra_info, :error_type)
+ end
+ end
+ end
+
+ class SummaryTestResult
+ def initialize(test_results:)
+ @test_results = test_results
+ end
+
+ def as_csv
+ results = {}
+ headers = Set.new
+
+ summary_test_results = @test_results.as_json
+
+ summary_test_results.each do |test_result|
+ header = "#{test_result['name']}:#{test_result['test_result_name']}"
+
+ if results.key?(test_result['group_name'])
+ results[test_result['group_name']][header] = test_result['status']
+ else
+ results[test_result['group_name']] = { header => test_result['status'] }
+ end
+
+ headers << header
+ end
+ headers = headers.sort
+
+ CSV.generate do |csv|
+ csv << [nil, *headers]
+
+ results.sort_by(&:first).each do |(group_name, _test_group)|
+ row = [group_name]
+
+ headers.each do |header|
+ if results[group_name].key?(header)
+ row << results[group_name][header]
+ else
+ row << nil
+ end
+ end
+ csv << row
+ end
+ end
+ end
+
+ def as_json
+ @test_results.group_by(&:group_name).transform_values do |grouping|
+ grouping.group_by(&:name)
+ end.to_json
+ end
+ end
+end
diff --git a/app/javascript/Components/Modals/download_test_results_modal.jsx b/app/javascript/Components/Modals/download_test_results_modal.jsx
index 35f12f18a0..33e42608d3 100644
--- a/app/javascript/Components/Modals/download_test_results_modal.jsx
+++ b/app/javascript/Components/Modals/download_test_results_modal.jsx
@@ -2,6 +2,16 @@ import React from "react";
import Modal from "react-modal";
class DownloadTestResultsModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ studentRun: true,
+ instructorRun: false,
+ latest: true,
+ format: "json",
+ };
+ }
+
componentDidMount() {
Modal.setAppElement("body");
}
@@ -18,49 +28,123 @@ class DownloadTestResultsModal extends React.Component {
item: I18n.t("activerecord.models.test_result.other"),
})}
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
diff --git a/app/javascript/Components/__tests__/download_test_results_modal.test.jsx b/app/javascript/Components/__tests__/download_test_results_modal.test.jsx
new file mode 100644
index 0000000000..ac4436f89b
--- /dev/null
+++ b/app/javascript/Components/__tests__/download_test_results_modal.test.jsx
@@ -0,0 +1,192 @@
+import * as React from "react";
+import {render, screen, fireEvent} from "@testing-library/react";
+import DownloadTestResultsModal from "../Modals/download_test_results_modal";
+import {beforeEach, describe, expect, it} from "@jest/globals";
+
+describe("DownloadTestResultsModal", () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ isOpen: true,
+ onRequestClose: jest.fn(),
+ course_id: 1,
+ assignment_id: 2,
+ };
+ });
+
+ describe("Initial state", () => {
+ it("should render the modal with correct title", () => {
+ render(
);
+ expect(
+ screen.getByText(
+ I18n.t("download_the", {item: I18n.t("activerecord.models.test_result.other")})
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("should render all radio button options", () => {
+ render(
);
+
+ expect(screen.getByLabelText(I18n.t("anyone"))).toBeInTheDocument();
+ expect(screen.getByLabelText(I18n.t("activerecord.models.student.one"))).toBeInTheDocument();
+ expect(
+ screen.getByLabelText(I18n.t("activerecord.models.instructor.one"))
+ ).toBeInTheDocument();
+
+ expect(screen.getByLabelText(I18n.t("all"))).toBeInTheDocument();
+ expect(screen.getByLabelText(I18n.t("latest"))).toBeInTheDocument();
+
+ expect(screen.getByLabelText(I18n.t("format.json"))).toBeInTheDocument();
+ expect(screen.getByLabelText(I18n.t("format.csv"))).toBeInTheDocument();
+ });
+ });
+
+ describe("Run by radio selection", () => {
+ it("should update state when Student is selected", () => {
+ render(
);
+
+ const studentRadio = screen.getByLabelText(I18n.t("activerecord.models.student.one"));
+ fireEvent.click(studentRadio);
+
+ expect(studentRadio).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("anyone"))).not.toBeChecked();
+ expect(screen.getByLabelText(I18n.t("activerecord.models.instructor.one"))).not.toBeChecked();
+ });
+
+ it("should update state when Instructor is selected", () => {
+ render(
);
+
+ const instructorRadio = screen.getByLabelText(I18n.t("activerecord.models.instructor.one"));
+ fireEvent.click(instructorRadio);
+
+ expect(instructorRadio).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("anyone"))).not.toBeChecked();
+ expect(screen.getByLabelText(I18n.t("activerecord.models.student.one"))).not.toBeChecked();
+ });
+
+ it("should allow switching back to Anyone", () => {
+ render(
);
+
+ const studentRadio = screen.getByLabelText(I18n.t("activerecord.models.student.one"));
+ fireEvent.click(studentRadio);
+ expect(studentRadio).toBeChecked();
+
+ const anyoneRadio = screen.getByLabelText(I18n.t("anyone"));
+ fireEvent.click(anyoneRadio);
+ expect(anyoneRadio).toBeChecked();
+ expect(studentRadio).not.toBeChecked();
+ });
+ });
+
+ describe("Type radio selection", () => {
+ it("should update state when Latest is selected", () => {
+ render(
);
+
+ const latestRadio = screen.getByLabelText(I18n.t("latest"));
+ fireEvent.click(latestRadio);
+
+ expect(latestRadio).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("all"))).not.toBeChecked();
+ });
+
+ it("should switch back to All when selected", () => {
+ render(
);
+
+ const latestRadio = screen.getByLabelText(I18n.t("latest"));
+ fireEvent.click(latestRadio);
+ expect(latestRadio).toBeChecked();
+
+ const allRadio = screen.getByLabelText(I18n.t("all"));
+ fireEvent.click(allRadio);
+ expect(allRadio).toBeChecked();
+ expect(latestRadio).not.toBeChecked();
+ });
+ });
+
+ describe("format radio selection", () => {
+ it("should update state when CSV is selected", () => {
+ render(
);
+
+ const csvRadio = screen.getByLabelText(I18n.t("format.csv"));
+ fireEvent.click(csvRadio);
+
+ expect(csvRadio).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("format.json"))).not.toBeChecked();
+ });
+
+ it("should switch back to JSON when selected", () => {
+ render(
);
+
+ const csvRadio = screen.getByLabelText(I18n.t("format.csv"));
+ fireEvent.click(csvRadio);
+ expect(csvRadio).toBeChecked();
+
+ const jsonRadio = screen.getByLabelText(I18n.t("format.json"));
+ fireEvent.click(jsonRadio);
+ expect(jsonRadio).toBeChecked();
+ expect(csvRadio).not.toBeChecked();
+ });
+
+ it("should enable CSV when Latest type is selected", () => {
+ render(
);
+
+ const latestRadio = screen.getByLabelText(I18n.t("latest"));
+ fireEvent.click(latestRadio);
+
+ const csvRadio = screen.getByLabelText(I18n.t("format.csv"));
+ expect(csvRadio).not.toBeDisabled();
+ });
+ });
+
+ describe("Modal interaction", () => {
+ it("should call onRequestClose when Cancel is clicked", () => {
+ render(
);
+
+ const cancelButton = screen.getByDisplayValue(I18n.t("cancel"));
+ fireEvent.click(cancelButton);
+
+ expect(props.onRequestClose).toHaveBeenCalled();
+ });
+
+ it("should call onRequestClose when Download is clicked", () => {
+ render(
);
+
+ const downloadButton = screen.getByText(I18n.t("download"));
+ fireEvent.click(downloadButton);
+
+ expect(props.onRequestClose).toHaveBeenCalled();
+ });
+
+ it("should handle multiple radio selections correctly", () => {
+ render(
);
+
+ fireEvent.click(screen.getByLabelText(I18n.t("activerecord.models.student.one")));
+ fireEvent.click(screen.getByLabelText(I18n.t("latest")));
+ fireEvent.click(screen.getByLabelText(I18n.t("format.csv")));
+
+ expect(screen.getByLabelText(I18n.t("activerecord.models.student.one"))).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("latest"))).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("format.csv"))).toBeChecked();
+
+ expect(screen.getByLabelText(I18n.t("anyone"))).not.toBeChecked();
+ expect(screen.getByLabelText(I18n.t("activerecord.models.instructor.one"))).not.toBeChecked();
+ expect(screen.getByLabelText(I18n.t("all"))).not.toBeChecked();
+ expect(screen.getByLabelText(I18n.t("format.json"))).not.toBeChecked();
+ });
+
+ it("should revert to JSON when switching from Latest to All", () => {
+ render(
);
+
+ fireEvent.click(screen.getByLabelText(I18n.t("latest")));
+ fireEvent.click(screen.getByLabelText(I18n.t("format.csv")));
+
+ expect(screen.getByLabelText(I18n.t("format.csv"))).toBeChecked();
+
+ fireEvent.click(screen.getByLabelText(I18n.t("all")));
+
+ expect(screen.getByLabelText(I18n.t("format.json"))).toBeChecked();
+ expect(screen.getByLabelText(I18n.t("format.csv"))).toBeDisabled();
+ });
+ });
+});
diff --git a/app/models/assignment.rb b/app/models/assignment.rb
index ba0303d5d6..5bb1fe25f7 100644
--- a/app/models/assignment.rb
+++ b/app/models/assignment.rb
@@ -667,78 +667,6 @@ def summary_json(user)
ltiDeployments: lti_deployments }
end
- # Generates the summary of the most test results associated with an assignment.
- def summary_test_results
- latest_test_run_by_grouping = TestRun.group('grouping_id').select('MAX(created_at) as test_runs_created_at',
- 'grouping_id')
- .where.not(submission_id: nil)
- .to_sql
-
- latest_test_runs = TestRun
- .joins(grouping: :group)
- .joins("INNER JOIN (#{latest_test_run_by_grouping}) latest_test_run_by_grouping \
- ON latest_test_run_by_grouping.grouping_id = test_runs.grouping_id \
- AND latest_test_run_by_grouping.test_runs_created_at = test_runs.created_at")
- .select('id', 'test_runs.grouping_id', 'groups.group_name')
- .to_sql
-
- self.test_groups.joins(test_group_results: :test_results)
- .joins("INNER JOIN (#{latest_test_runs}) latest_test_runs \
- ON test_group_results.test_run_id = latest_test_runs.id")
- .select('test_groups.name',
- 'test_groups.id as test_groups_id',
- 'latest_test_runs.group_name',
- 'test_results.name as test_result_name',
- 'test_results.status',
- 'test_results.marks_earned',
- 'test_results.marks_total',
- :output, :extra_info, :error_type)
- end
-
- # Generate a JSON summary of the most recent test results associated with an assignment.
- def summary_test_result_json
- self.summary_test_results.group_by(&:group_name).transform_values do |grouping|
- grouping.group_by(&:name)
- end.to_json
- end
-
- # Generate a CSV summary of the most recent test results associated with an assignment.
- def summary_test_result_csv
- results = {}
- headers = Set.new
- summary_test_results = self.summary_test_results.as_json
-
- summary_test_results.each do |test_result|
- header = "#{test_result['name']}:#{test_result['test_result_name']}"
-
- if results.key?(test_result['group_name'])
- results[test_result['group_name']][header] = test_result['status']
- else
- results[test_result['group_name']] = { header => test_result['status'] }
- end
-
- headers << header
- end
- headers = headers.sort
-
- CSV.generate do |csv|
- csv << [nil, *headers]
-
- results.sort_by(&:first).each do |(group_name, _test_group)|
- row = [group_name]
-
- headers.each do |header|
- if results[group_name].key?(header)
- row << results[group_name][header]
- else
- row << nil
- end
- end
- csv << row
- end
- end
- end
-
# Generate CSV summary of grades for this assignment
# for the current user. The user should be an instructor or TA.
def summary_csv(role)
diff --git a/app/models/test_run.rb b/app/models/test_run.rb
index b6069d6a16..53c2055466 100644
--- a/app/models/test_run.rb
+++ b/app/models/test_run.rb
@@ -13,6 +13,9 @@ class TestRun < ApplicationRecord
validate :autotest_test_id_uniqueness
before_save :unset_autotest_test_id
+ scope :student_run, -> { where.not(submission_id: nil) }
+ scope :instructor_run, -> { where(submission_id: nil) }
+
SETTINGS_FILES_DIR = (Settings.file_storage.autotest || File.join(Settings.file_storage.default_root_path,
'autotest')).freeze
SPECS_FILE = 'specs.json'.freeze
diff --git a/config/locales/common/en.yml b/config/locales/common/en.yml
index 8eb23a2fb9..f2650bc13c 100644
--- a/config/locales/common/en.yml
+++ b/config/locales/common/en.yml
@@ -4,6 +4,7 @@ en:
add: Add
additional_not_shown: Additional %{count} not shown.
all: All
+ anyone: Anyone
apply: Apply
cancel: Cancel
clear_all: Clear All
@@ -17,9 +18,14 @@ en:
errors:
invalid_path: Invalid path provided.
file: File
+ file_format: File format
filter_by: Filter by %{name}
forbidden: Forbidden
+ format:
+ csv: CSV
+ json: JSON
help: Help
+ latest: Latest
lti:
config_error: Error configuring LTI.
course_exists: A course with this name already exists on MarkUs. Please select a course to link to.
diff --git a/config/locales/defaults/download_upload/en.yml b/config/locales/defaults/download_upload/en.yml
index a135aa5214..dbb1411cdc 100644
--- a/config/locales/defaults/download_upload/en.yml
+++ b/config/locales/defaults/download_upload/en.yml
@@ -5,7 +5,6 @@ en:
download_csv: Download in CSV Format
download_errors:
unrecognized_format: Could not recognize %{format} format to download with
- download_json: Download in JSON Format
download_the: Download %{item}
download_yml: Download in YML Format
print_the: Print %{item}
diff --git a/config/locales/models/test_runs/en.yml b/config/locales/models/test_runs/en.yml
index f1e5f4df94..8f4927e107 100644
--- a/config/locales/models/test_runs/en.yml
+++ b/config/locales/models/test_runs/en.yml
@@ -4,6 +4,7 @@ en:
attributes:
test_run:
status: Status
+ type: Submission time
user: Run by
models:
test_run:
diff --git a/spec/controllers/assignments_controller_spec.rb b/spec/controllers/assignments_controller_spec.rb
index d731c3434c..1af87da294 100644
--- a/spec/controllers/assignments_controller_spec.rb
+++ b/spec/controllers/assignments_controller_spec.rb
@@ -108,34 +108,70 @@
let(:assignment) { create(:assignment_with_criteria_and_test_results) }
it 'responds with the appropriate status' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'json'
expect(response).to have_http_status :success
end
it 'responds with the appropriate header' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'json'
expect(response.header['Content-Type']).to eq('application/json')
end
it 'sets disposition as attachment' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'json'
d = response.header['Content-Disposition'].split.first
expect(d).to eq 'attachment;'
end
it 'responds with the appropriate filename' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'json'
filename = response.header['Content-Disposition'].split[1].split('"').second
expect(filename).to eq("#{assignment.short_identifier}_test_results.json")
end
it 'returns application/json type' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'json'
expect(response.media_type).to eq 'application/json'
end
it 'returns the most recent test results' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'json'
body = response.parsed_body
# We want to ensure that the test result's group name, test name and status exists
@@ -156,29 +192,59 @@
let(:assignment) { create(:assignment_with_criteria_and_test_results) }
it 'responds with the appropriate status' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
expect(response).to have_http_status :success
end
it 'sets disposition as attachment' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
d = response.header['Content-Disposition'].split.first
expect(d).to eq 'attachment;'
end
it 'responds with the appropriate filename' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
filename = response.header['Content-Disposition'].split[1].split('"').second
expect(filename).to eq("#{assignment.short_identifier}_test_results.csv")
end
it 'returns text/csv type' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
expect(response.media_type).to eq 'text/csv'
end
it 'returns the most recent test results of the correct size' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
test_results = CSV.parse(response.body, headers: true)
@@ -187,10 +253,21 @@
end
it 'returns the correct csv headers' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
test_results = CSV.parse(response.body, headers: true)
- assignment_results = assignment.summary_test_results
+ assignment_results = SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ ).instance_variable_get(:@test_results)
headers = Set.new(test_results.headers.drop(1)).sort
assignment_results.each do |result|
@@ -199,7 +276,13 @@
end
it 'returns the correct csv headers in the correct order' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
test_results = CSV.parse(response.body, headers: true)
headers = test_results.headers.drop(1)
@@ -210,7 +293,13 @@
end
it 'returns the correct amount of passed tests per group' do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
test_results = CSV.parse(response.body, headers: true).to_a.drop(1)
test_results.to_a.each do |row|
count = 0
@@ -230,7 +319,13 @@
let(:csv_results) { CSV.parse(response.body, headers: true) }
before do
- get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
+ get_as user, :download_test_results, params: {
+ course_id: user.course.id,
+ id: assignment.id,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ }, format: 'csv'
end
it 'returns the correct headers' do
@@ -2351,4 +2446,23 @@
expect(response).to redirect_to(edit_course_assignment_path(course.id, assignment.id))
end
end
+
+ describe '#download_test_results' do
+ let(:course) { assignment.course }
+ let(:assignment) { create(:assignment_with_criteria_and_test_results) }
+ let(:instructor) { create(:instructor) }
+
+ context 'when latest is false and format is json' do
+ it 'should send a zip file' do
+ get_as instructor, :download_test_results,
+ params: { course_id: course.id, id: assignment.id, latest: 'false' },
+ format: :json
+
+ expect(response).to have_http_status(:ok)
+ expect(response.headers['Content-Type']).to eq('application/zip')
+ expect(response.headers['Content-Disposition']).to include('attachment')
+ expect(response.headers['Content-Disposition']).to include("#{assignment.short_identifier}_test_results.zip")
+ end
+ end
+ end
end
diff --git a/spec/helpers/summary_test_results_helper_spec.rb b/spec/helpers/summary_test_results_helper_spec.rb
new file mode 100644
index 0000000000..df3c74af3c
--- /dev/null
+++ b/spec/helpers/summary_test_results_helper_spec.rb
@@ -0,0 +1,113 @@
+describe SummaryTestResultsHelper do
+ describe SummaryTestResultsHelper::SummaryTestResults do
+ context 'an assignment with no test results' do
+ let(:assignment) { create(:assignment_with_criteria_and_results) }
+
+ it 'should return {} when using json' do
+ expect(SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ ).as_json).to eq('{}')
+ end
+
+ it 'should be empty when using csv' do
+ expect(SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ ).as_csv).to eq("\n")
+ end
+ end
+
+ context 'an assignment with test results across multiple test groups' do
+ let(:assignment) { create(:assignment_with_criteria_and_test_results) }
+
+ it 'has the correct group and test names' do
+ expect(assignment.test_groups.size).to be > 1
+
+ summary_test_results = JSON.parse(SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ ).as_json)
+
+ summary_test_results.map do |group_name, group|
+ group.map do |test_group_name, test_group|
+ test_group.each do |test_result|
+ expect(test_result.fetch('name')).to eq test_group_name
+ expect(test_result.fetch('group_name')).to eq group_name
+ expect(test_result.key?('status')).to be true
+ end
+ end
+ end
+ end
+
+ it 'has the correct test result keys' do
+ expect(assignment.test_groups.size).to be > 1
+
+ summary_test_results = JSON.parse(SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ ).as_json)
+
+ expected_keys = %w[marks_earned
+ marks_total
+ output
+ name
+ test_result_name
+ test_groups_id
+ group_name
+ status
+ extra_info
+ error_type
+ id]
+ summary_test_results.map do |_, group|
+ group.map do |_, test_group|
+ test_group.each do |test_result|
+ expect(test_result.keys).to match_array expected_keys
+ end
+ end
+ end
+ end
+
+ # despite having multiple test groups, assignment is set up so every test
+ # run contains results from exactly one test group; so this should also
+ # return results from only one test group
+ it 'returns results from only one test group for each group when fetching latest results' do
+ expect(assignment.test_groups.size).to be > 1
+
+ summary_test_results = JSON.parse(SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: true,
+ student_run: true,
+ instructor_run: false
+ ).as_json)
+
+ summary_test_results.map do |_group_name, group|
+ expect(group.count).to eq 1
+ end
+ end
+
+ it 'returns results from more than one test group for each group when not fetching latest results' do
+ expect(assignment.test_groups.size).to be > 1
+
+ summary_test_results = JSON.parse(SummaryTestResultsHelper::SummaryTestResults.fetch(
+ test_groups: assignment.test_groups,
+ latest: false,
+ student_run: true,
+ instructor_run: false
+ ).as_json)
+
+ summary_test_results.map do |_group_name, group|
+ expect(group.count).to be > 1
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb
index 86c3ecbf48..fc42220459 100644
--- a/spec/models/assignment_spec.rb
+++ b/spec/models/assignment_spec.rb
@@ -2164,70 +2164,6 @@ def grouping_count(groupings)
end
end
- describe '#summary_test_results' do
- context 'an assignment with no test results' do
- let(:assignment) { create(:assignment_with_criteria_and_results) }
-
- it 'should return {}' do
- summary_test_results = assignment.summary_test_results
- expect(summary_test_results).to be_empty
- end
- end
-
- context 'an assignment with test results across multiple test groups' do
- let(:assignment) { create(:assignment_with_criteria_and_test_results) }
-
- it 'has the correct group and test names' do
- summary_test_results = JSON.parse(assignment.summary_test_result_json)
- summary_test_results.map do |group_name, group|
- group.map do |test_group_name, test_group|
- test_group.each do |test_result|
- expect(test_result.fetch('name')).to eq test_group_name
- expect(test_result.fetch('group_name')).to eq group_name
- expect(test_result.key?('status')).to be true
- end
- end
- end
- end
-
- it 'has the correct test result keys' do
- summary_test_results = JSON.parse(assignment.summary_test_result_json)
- expected_keys = %w[marks_earned
- marks_total
- output
- name
- test_result_name
- test_groups_id
- group_name
- status
- extra_info
- error_type
- id]
- summary_test_results.map do |_, group|
- group.map do |_, test_group|
- test_group.each do |test_result|
- expect(test_result.keys).to match_array expected_keys
- end
- end
- end
- end
-
- it 'has multiple test groups' do
- expect(assignment.test_groups.size).to be > 1
- end
-
- # despite having multiple test groups, assignment is set up so every test
- # run contains results from exactly one test group; so this should also
- # return results from only one test group
- it 'returns results from only one test group for each group' do
- summary_test_results = JSON.parse(assignment.summary_test_result_json)
- summary_test_results.map do |_group_name, group|
- expect(group.count).to eq 1
- end
- end
- end
- end
-
describe '#summary_json' do
context 'a Student user' do
let(:assignment) { create(:assignment) }