Skip to content
Merged
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
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Add API endpoint `add_test_run` that allows independent user submissions of test executions to MarkUs (#7730)
- Display timeout status for autotest runs in the Test Results table. (#7734)
- Assign extra marks in test definition. (Currently limited to pytest files) (#7728)
- Enable zip downloads of test results (#7733)

### 🐛 Bug fixes
- Fix name column search in graders table (#7693)
Expand Down
16 changes: 16 additions & 0 deletions app/controllers/assignments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,22 @@ def download_test_results
type: 'text/csv',
filename: filename
end
format.zip do
data = @assignment.summary_test_result_json
zip_name = "#{@assignment.short_identifier}_test_results.zip"

zip_path = File.join('tmp', zip_name)
json_filename = "#{@assignment.short_identifier}_test_results.json"

FileUtils.rm_f(zip_path)

Zip::File.open(zip_path, create: true) do |zip_file|
zip_file.get_output_stream(json_filename) do |f|
f.write(data)
end
end
send_file zip_path, filename: zip_name, type: 'application/zip', disposition: 'attachment'
end
end
end

Expand Down
17 changes: 17 additions & 0 deletions app/javascript/Components/Modals/download_test_results_modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ class DownloadTestResultsModal extends React.Component {
{I18n.t("download_csv")}
</button>
</a>
<a
href={Routes.download_test_results_course_assignment_path({
course_id: this.props.course_id,
id: this.props.assignment_id,
format: "zip",
_options: true,
})}
>
<button
type="submit"
name="download-test-results-zip"
onClick={this.props.onRequestClose}
>
<i className="fa-solid fa-download" aria-hidden="true" />
{I18n.t("download_zip")}
</button>
</a>
<section className="dialog-actions">
<input
onClick={this.props.onRequestClose}
Expand Down
1 change: 1 addition & 0 deletions config/locales/defaults/download_upload/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ en:
download_json: Download in JSON Format
download_the: Download %{item}
download_yml: Download in YML Format
download_zip: Download in ZIP Format
print_the: Print %{item}
rename: Rename
submit_the: Submit %{item}
Expand Down
74 changes: 74 additions & 0 deletions spec/controllers/assignments_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,80 @@
end
end

context 'download the most recent test results as ZIP' do
let(:user) { create(:instructor) }
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: 'zip'
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: 'zip'
expect(response.header['Content-Type']).to eq('application/zip')
end

it 'sets disposition as attachment' do
get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'zip'
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: 'zip'
filename = response.header['Content-Disposition'].split[1].split('"').second
expect(filename).to eq("#{assignment.short_identifier}_test_results.zip")
end

it 'returns application/zip type' do
get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'zip'
expect(response.media_type).to eq 'application/zip'
end

it 'contains a JSON file with the correct filename' do
get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'zip'

Tempfile.open(['test_results', '.zip']) do |temp_file|
temp_file.binmode
temp_file.write(response.body)
temp_file.rewind

Zip::File.open(temp_file.path) do |zip_file|
expected_json_filename = "#{assignment.short_identifier}_test_results.json"
expect(zip_file.entries.map(&:name)).to include(expected_json_filename)
end
end
end

it 'contains valid JSON data inside the zip' do
get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'zip'

Tempfile.open(['test_results', '.zip']) do |temp_file|
temp_file.binmode
temp_file.write(response.body)
temp_file.rewind

Zip::File.open(temp_file.path) do |zip_file|
json_filename = "#{assignment.short_identifier}_test_results.json"
json_content = zip_file.read(json_filename)
parsed_json = JSON.parse(json_content)

# Verify the JSON structure matches what we expect
parsed_json.each do |group_name, group|
group.each 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
end
end
end

describe '#index' do
let(:course) { role.course }

Expand Down